代码:

<template>
	<view>
		<view 
			class="topBtn" 
			:class="{ 'disabled': disabled }"
			:style="{
				transform: `translate(${position.x}px, ${position.y}px)`,
				...defineStyle,
			}"
			@touchstart="onTouchStart" 
			@touchmove="onTouchMove" 
			@touchend="onTouchEnd" 
			@click="handleClick"
		>
			<slot></slot>
		</view>
	</view>
</template>

<script>
	export default {
		props: {
			disabled: {
				type: Boolean,
				default: false,
			},
			draggable: {
				type: Boolean,
				default: true,
			},
			defineStyle: {
				type: Object,
				default: () => ({}),
			},
		},
		data() {
			return {
				isFirstClick: true,
				hideTimer: null,

				// 拖拽相关
				startTouch: { x: 0, y: 0 },
				startPosition: { x: 0, y: 0 },
				position: { x: 0, y: 0 },
				isDragging: false,
				dragThreshold: 5,

				// 屏幕旋转适配
				lastWindowWidth: 0,
				lastWindowHeight: 0,

				// 性能优化:缓存系统信息 & 按钮尺寸
				sysInfo: null,
				btnSize: 100, // 必须与 CSS 中 width/height 一致(80px?注意!)
			};
		},
		mounted() {
			this.initPosition();
			uni.onWindowResize(this.handleWindowResize);
		},
		beforeDestroy() {
			if (this.hideTimer) clearTimeout(this.hideTimer);
			uni.offWindowResize(this.handleWindowResize);
		},
		methods: {
			initPosition() {
				const sys = uni.getSystemInfoSync();
				this.sysInfo = sys;

				const margin = 30;
				const x = sys.windowWidth - this.btnSize - margin;
				const y = sys.windowHeight * 0.85 - this.btnSize;

				this.position = {
					x: Math.round(Math.max(0, Math.min(sys.windowWidth - this.btnSize, x))),
					y: Math.round(Math.max(0, Math.min(sys.windowHeight - this.btnSize, y)))
				};

				this.lastWindowWidth = sys.windowWidth;
				this.lastWindowHeight = sys.windowHeight;
			},

			handleWindowResize() {
				const sys = uni.getSystemInfoSync();
				this.sysInfo = sys; // 👈 更新缓存

				if (this.lastWindowWidth === 0 || this.lastWindowHeight === 0) {
					this.lastWindowWidth = sys.windowWidth;
					this.lastWindowHeight = sys.windowHeight;
					return;
				}

				const ratioX = this.position.x / this.lastWindowWidth;
				const ratioY = this.position.y / this.lastWindowHeight;

				let newX = ratioX * sys.windowWidth;
				let newY = ratioY * sys.windowHeight;

				newX = Math.max(0, Math.min(sys.windowWidth - this.btnSize, newX));
				newY = Math.max(0, Math.min(sys.windowHeight - this.btnSize, newY));

				this.position = {
					x: Math.round(newX),
					y: Math.round(newY)
				};

				this.lastWindowWidth = sys.windowWidth;
				this.lastWindowHeight = sys.windowHeight;
			},

			onTouchStart(e) {
				if (!this.draggable) return;
				const touch = e.touches[0];
				this.startTouch.x = touch.clientX;
				this.startTouch.y = touch.clientY;
				this.startPosition = { ...this.position };
				this.isDragging = false;
			},

			onTouchMove(e) {
				if (!this.draggable) return;
				const touch = e.touches[0];
				const deltaX = touch.clientX - this.startTouch.x;
				const deltaY = touch.clientY - this.startTouch.y;

				let newX = this.startPosition.x + deltaX;
				let newY = this.startPosition.y + deltaY;

				const sys = this.sysInfo;
				newX = Math.max(0, Math.min(sys.windowWidth - this.btnSize, newX));
				newY = Math.max(0, Math.min(sys.windowHeight - this.btnSize, newY));

				this.position = { x: newX, y: newY };
				this.isDragging = true;
			},

			onTouchEnd(e) {
				if (!this.draggable) return;

				const touch = e.changedTouches[0];
				const dx = touch.clientX - this.startTouch.x;
				const dy = touch.clientY - this.startTouch.y;
				const distance = Math.sqrt(dx * dx + dy * dy);

				// 移动很小视为点击(保留注释逻辑,但未启用)
				// if (distance < this.dragThreshold) {
				//   this.handleClick();
				// }
			},

			handleClick() {
				if (this.disabled) return;
				this.$emit('click');

				if (this.hideTimer) clearTimeout(this.hideTimer);

				if (this.isFirstClick) {
					this.isFirstClick = false;
					this.hideTimer = setTimeout(() => {
						this.isFirstClick = true;
					}, 5000);
				} else {
					uni.pageScrollTo({
						scrollTop: 0,
						duration: 300,
						success: () => {
							setTimeout(() => {
								this.isFirstClick = true;
							}, 500);
						}
					});
				}
			}
		}
	};
</script>

<style lang="scss" scoped>
	.topBtn {
		/* 关键:固定在左上角,由 transform 控制位置 */
		position: fixed !important;
		top: 0 !important;
		left: 0 !important;
		z-index: 9999 !important;

		width: 80px !important;
		height: 80px !important;
		font-size: 18px !important;

		background: #3c9cff !important;
		border-radius: 50% !important;
		display: flex !important;
		align-items: center !important;
		justify-content: center !important;
		box-shadow: 0 2px 6px rgba(60, 156, 255, 0.6) !important;
		cursor: pointer !important;
		user-select: none !important;
		touch-action: none !important; /* 禁用默认手势 */
	}
	.topBtn:active{
		background: #3c9cffaa !important;
	}

	/* 脉冲光环 */
	.topBtn::before {
		content: '';
		position: absolute;
		top: 0;
		left: 0;
		width: 80px;
		height: 80px;
		border-radius: 50%;
		background: #3c9cff;
		opacity: 0.6;
		animation: pulseRing 2s infinite ease-out;
		z-index: -1;
	}

	@keyframes pulseRing {
		0% {
			transform: scale(0.8);
			opacity: 0.6;
		}
		70% {
			transform: scale(1.4);
			opacity: 0.2;
		}
		100% {
			transform: scale(1.6);
			opacity: 0;
		}
	}

	.topBtn.disabled::before {
		visibility: hidden;
	}

	.topBtn.disabled {
		background: #78bbff !important;
		/* 注意:未禁用 pointer-events,确保 draggable=true 时仍可拖 */
	}
</style>

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐