Skip to content

使用coolunix 写了个浮动面板 希望可以采纳 #16

@wayyll

Description

@wayyll
组件代码
<template>
	<!-- 遮罩层 -->
	<view :style="{height:isExpanded?`${heightPx}px`:'0'}"
		class="fixed top-0 left-0 right-0 bottom-0 z-[0] bg-transparent bg-opacity-30 transition-opacity duration-150 ease-in-out "
		@click="closePanel"></view>
	<view class="fixed bottom-0 left-0 right-0" :class="{ 'transition-all duration-150 ease-out': !isDragging }"
		:style="{ bottom: bottomHeight + 'px', height: `${Math.max(minHeight, maxHeight - translateY)}rpx` }">

		<!-- 浮动面板 -->
		<view class="absolute bottom-0 left-0 right-0 z-[30] rounded-t-[40rpx] z-[30] shadow-2xl pointer-events-auto"
			:class="{ 'transition-transform duration-150 ease-in-out': !isDragging }"
			:style="{ transform: `translateY(${translateY}rpx)`, height: `${maxHeight}rpx` }">
			<!-- 拖拽头部 -->
			<view @touchstart.prevent="onTouchStart" @touchmove.prevent="onTouchMove" @touchend.prevent="onTouchEnd">
				<slot name="head">
					<view
						class="relative flex flex-col items-center justify-center bg-white rounded-t-[40rpx] h-[60rpx] py-[20rpx] border-b border-gray-100">
						<!-- 拖拽指示器 -->
						<view class="bg-gray-300 rounded-full  w-[80rpx] h-[8rpx]"></view>
					</view>
				</slot>
			</view>
			<!-- 面板内容 -->
			<scroll-view class="flex-1 p-[40rpx] bg-white" @touchstart.stop @touchmove.stop @touchend.stop>
				<slot></slot>
			</scroll-view>
		</view>

	</view>
</template>

<script lang="ts" setup>
	import { ref, computed, onMounted } from 'vue'

	defineOptions({
		name: "float-panel"
	});

	type Props = {
		maxHeight ?: number;
		minHeight ?: number;
		bottomHeight ?: number;
	}

	const props = withDefaults(defineProps<Props>(), {
		maxHeight: 1200, // 最大高度 rpx
		minHeight: 60,     // 最小高度(头部高度)rpx
		bottomHeight: 0
	})
	const heightPx = computed(() => {
		let window = uni.getWindowInfo()
		return window.screenHeight - props.bottomHeight
	})
	const emits = defineEmits(['getStatus'])
	// 解构props以便在模板中使用
	const { maxHeight, minHeight, bottomHeight } = props

	// 面板状态
	const translateY = ref(props.maxHeight - props.minHeight) // 初始位置
	const isExpanded = ref(false)
	const isDragging = ref(false)

	// 触摸相关
	const startY = ref(0)
	const startTranslateY = ref(0)
	const currentY = ref(0)
	// 记录时间戳
	const startTime = ref(0)
	const endTime = ref(0)

	// 计算面板是否展开
	const panelExpanded = computed(() => {
		return translateY.value < (props.maxHeight - props.minHeight) / 2
	})
	// 展开面板
	function expandPanel() {
		translateY.value = 0
		isExpanded.value = true
		emits('getStatus', true)
	}
	// 收起面板
	function collapsePanel() {
		translateY.value = props.maxHeight - props.minHeight
		isExpanded.value = false
		emits('getStatus', false)
	}

	// 关闭面板(点击遮罩)
	function closePanel() {
		collapsePanel()
	}

	// 触摸开始
	function onTouchStart(e : UniTouchEvent) {
		isDragging.value = true
		startY.value = e.touches[0].clientY
		startTranslateY.value = translateY.value
		currentY.value = startY.value
		startTime.value = Date.now()
	}

	// 触摸移动
	function onTouchMove(e : UniTouchEvent) {
		if (!isDragging.value) return

		currentY.value = e.touches[0].clientY
		const deltaY = currentY.value - startY.value
		let newTranslateY = startTranslateY.value + deltaY

		// 限制拖拽范围
		const maxTranslateY = props.maxHeight - props.minHeight
		const minTranslateY = 0

		// 严格限制在范围内,不允许超出
		if (newTranslateY > maxTranslateY) {
			newTranslateY = maxTranslateY
		} else if (newTranslateY < minTranslateY) {
			newTranslateY = minTranslateY
		}

		translateY.value = newTranslateY

		// 实时更新isExpanded状态用于遮罩层显示
		const threshold = (props.maxHeight - props.minHeight) * 0.3
		isExpanded.value = newTranslateY < threshold
	}
	// 计算滑动速度
	function calculateVelocity() : number {
		const deltaY = currentY.value - startY.value
		const deltaTime = endTime.value - startTime.value
		return deltaY / Math.max(deltaTime, 16) // 最小16ms防止除零
	}
	// 切换面板状态(点击头部)
	function togglePanel() {
		if (isExpanded.value) {
			collapsePanel()
		} else {
			expandPanel()
		}
	}
	// 触摸结束
	function onTouchEnd(e : UniTouchEvent) {
		if (!isDragging.value) return

		isDragging.value = false
		endTime.value = Date.now()

		const deltaY = Math.abs(currentY.value - startY.value)
		const deltaTime = endTime.value - startTime.value

		// 判断是否为点击(移动距离小且时间短)
		if (deltaY < 10 && deltaTime < 300) {
			// 点击行为,切换面板状态
			togglePanel()
			return
		}

		const velocity = calculateVelocity()
		const threshold = (props.maxHeight - props.minHeight) / 2

		// 根据速度和位置决定最终状态
		if (velocity > 0.5) {
			// 向下滑动,收起面板
			collapsePanel()
		} else if (velocity < -0.5) {
			// 向上滑动,展开面板
			expandPanel()
		} else if (translateY.value > threshold) {
			// 位置超过阈值,收起面板
			collapsePanel()
		} else {
			// 位置未超过阈值,展开面板
			expandPanel()
		}
	}





	// 暴露方法给父组件
	defineExpose({
		expandPanel,
		collapsePanel,
		isExpanded: computed(() => isExpanded.value)
	})

	onMounted(() => {
		// 初始化状态
		isExpanded.value = false
	})
</script>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions