前端开发中的防误触按钮的形式:滑动触发事件 长摁触发事件
滑动触发事件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
body {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
gap: 50px;
}
.track {
position: relative;
width: 500px;
height: 50px;
line-height: 50px;
text-align: center;
background-color: #ccc;
border-radius: 5px;
overflow: hidden;
font-size: 32px;
}
.progress {
position: absolute;
top: 0;
left: 0;
height: 100%;
background-color: skyblue;
}
.slider {
position: absolute;
top: 0;
left: 0;
width: 50px;
height: 50px;
background-color: lemonchiffon;
cursor: pointer;
}
.act-info {
position: absolute;
}
</style>
</head>
<body>
<div class="track">
<div class="progress"></div>
<div class="slider"></div>
<span class="act-info">拖动触发事件</span>
</div>
<script>
const track = document.querySelector(".track");
const slider = document.querySelector(".slider");
const progress = document.querySelector(".progress");
let isDragging = false;
let startX, startLeft;
let isComplete = false;
// 记录初始位置
const initialPosition = 0;
// 触摸事件处理
slider.addEventListener("touchstart", startDrag, { passive: false });
document.addEventListener("touchmove", handleDrag, { passive: false });
document.addEventListener("touchend", endDrag);
// 鼠标事件处理
slider.addEventListener("mousedown", startDrag);
document.addEventListener("mousemove", handleDrag);
document.addEventListener("mouseup", endDrag);
function startDrag(e) {
console.log("drag start");
isDragging = true;
slider.style.transition = "none";
// 获取初始位置,兼容鼠标和触摸事件
const clientX = e.type.includes("mouse")
? e.clientX
: e.touches[0].clientX;
startX = clientX;
startLeft = slider.getBoundingClientRect().left;
isComplete = false;
// 阻止默认行为,防止页面滚动
if (e.type.includes("touch")) {
e.preventDefault();
}
}
function handleDrag(e) {
if (!isDragging) return;
console.log("dragging");
// 阻止默认行为,防止页面滚动
if (e.type.includes("touch")) {
e.preventDefault();
}
const wrapperRect = track.getBoundingClientRect();
// 获取当前位置,兼容鼠标和触摸事件
const clientX = e.type.includes("mouse")
? e.clientX
: e.touches[0].clientX;
const newX = Math.min(
Math.max(clientX - startX + startLeft - wrapperRect.left, 0),
wrapperRect.width - slider.offsetWidth
);
slider.style.left = `${newX}px`;
progress.style.width = `${(newX / wrapperRect.width) * 100}%`;
// 检查是否到达最右侧
if (newX === wrapperRect.width - slider.offsetWidth) {
isComplete = true;
alert("Done");
} else {
isComplete = false;
}
}
function endDrag() {
console.log("drag end");
if (!isDragging) return;
isDragging = false;
slider.style.transition = "left 0.2s ease";
// 如果没有完成,重置位置
if (!isComplete) {
slider.style.left = `${initialPosition}px`;
progress.style.width = "0%";
}
}
</script>
</body>
</html>
<template>
<div
ref="container"
class="relative w-full h-[48px] bg-[#f2f2f2] rounded-[12px] border border-[#accbc3] overflow-hidden select-none flex items-center shadow-[0_4px_6px_-1px_rgba(0,0,0,0.02)]"
>
<!-- 背景进度填充 -->
<div
class="absolute inset-y-0 left-0 bg-[#8fc54b] pointer-events-none transition-opacity"
:style="{ width: `${currentX + sliderWidth / 2}px`, opacity: currentX > 0 ? 1 : 0 }"
></div>
<!-- 中间引导文字 -->
<div
class="absolute inset-0 flex items-center justify-center pointer-events-none transition-opacity duration-200"
:style="{ opacity: textOpacity }"
>
<span class="text-[#648334] font-medium text-lg tracking-wide">
<slot />
</span>
</div>
<!-- 可拖拽滑块 -->
<div
ref="slider"
@pointerdown="onPointerDown"
class="z-10 px-[14px] h-full w-fit bg-gradient-to-r from-[#aadd5d] to-[#87c245] rounded-tr-[12px] rounded-br-[12px] rounded-tl-[8px] rounded-bl-[8px] flex items-center justify-center shadow-[0_1px_4px_rgba(0, 0, 0, 0.1)] touch-none cursor-grab active:cursor-grabbing"
:class="{ 'transition-transform duration-300': !isDragging }"
:style="{ transform: `translateX(${currentX}px)` }"
>
<ChevronsRight class="text-[#fff] w-8 h-8" :stroke-width="2.5" />
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
import { ChevronsRight } from 'lucide-vue-next';
const emit = defineEmits(['finish']);
const container = ref(null);
const slider = ref(null);
const currentX = ref(0);
const isDragging = ref(false);
const startX = ref(0);
const maxDrag = ref(0);
const sliderWidth = ref(0);
const hasFinished = ref(false);
// 文字透明度:滑块移动超过容器宽度的30%时开始淡出
const textOpacity = computed(() => {
if (maxDrag.value === 0) return 1;
const ratio = currentX.value / maxDrag.value;
return Math.max(0, 1 - ratio * 1.5); // 1.5倍速淡出,更灵敏
});
// 计算可拖动最大距离(滑块右边缘贴到容器右边缘)
const updateDimensions = () => {
if (container.value && slider.value) {
const containerWidth = container.value.getBoundingClientRect().width;
sliderWidth.value = slider.value.getBoundingClientRect().width;
// 最大拖动距离 = 容器宽度 - 滑块宽度
maxDrag.value = Math.max(0, containerWidth - sliderWidth.value);
}
};
const onPointerDown = (e) => {
if (hasFinished.value) return; // 完成后禁止拖动
isDragging.value = true;
startX.value = e.clientX - currentX.value;
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerUp);
// 防止拖动时选中文字
window.addEventListener('pointercancel', onPointerUp);
};
const onPointerMove = (e) => {
if (!isDragging.value) return;
let x = e.clientX - startX.value;
// 限制在 [0, maxDrag] 范围内
x = Math.max(0, Math.min(x, maxDrag.value));
currentX.value = x;
};
const onPointerUp = () => {
isDragging.value = false;
window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerup', onPointerUp);
window.removeEventListener('pointercancel', onPointerUp);
// 完成阈值:拖动超过最大距离的 85% 视为完成(更自然的手感)
const threshold = maxDrag.value * 0.85;
if (currentX.value >= threshold && maxDrag.value > 0) {
// 吸附到最右侧
currentX.value = maxDrag.value;
hasFinished.value = true;
emit('finish');
} else {
// 回弹到起点
currentX.value = 0;
}
};
function resetButton() {
currentX.value = 0;
hasFinished.value = false;
}
onMounted(() => {
// nextTick 确保 DOM 渲染完成后再计算尺寸
nextTick(updateDimensions);
window.addEventListener('resize', updateDimensions);
});
onUnmounted(() => {
window.removeEventListener('resize', updateDimensions);
window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerup', onPointerUp);
window.removeEventListener('pointercancel', onPointerUp);
});
defineExpose({
resetButton,
});
</script>
<style scoped>
.cursor-grab {
cursor: grab;
}
.cursor-grabbing {
cursor: grabbing;
}
</style>
长摁触发事件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>逐渐拼凑的长方形进度条</title>
<style>
@import url("https://fonts.googleapis.com/css?family=Lato:700");
* {
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #ecf0f1;
font-family: "Lato", sans-serif;
}
.progress-btn-wrapper {
width: 300px;
height: 150px;
position: relative;
}
.progress {
width: 100%;
height: 100%;
border-radius: 5px;
position: absolute;
background: conic-gradient(
#3498db 0deg,
#3498db 0deg,
transparent 0deg
);
transition: background 0.5s linear; /* Smooth transition */
}
.btn {
position: absolute;
width: 270px;
height: 120px;
border-radius: 5px;
background: #34495e;
top: 15px;
left: 15px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 2em;
}
.progress-text {
font-size: 1.5em;
}
</style>
</head>
<body>
<div class="progress-btn-wrapper">
<div class="progress"></div>
<div class="btn">
<span class="progress-text">0%</span>
</div>
</div>
<script>
const progressText = document.querySelector(".progress-text");
const progress = document.querySelector(".progress");
const btn = document.querySelector(".btn");
const duration = 2000;
const step = 5;
const interval = duration / (100 / step);
let progressTimer = null;
let percent = 0;
// 按下时开始进度条
btn.addEventListener("touchstart", (e) => {
e.preventDefault();
if (progressTimer) return; // 防止重复启动
timer = setTimeout(() => {
complete();
}, duration);
progressTimer = setInterval(() => {
percent += step;
if (percent > 100) percent = 100;
updateProgress(percent);
}, interval);
});
// 松开时平滑回退进度条
btn.addEventListener("touchend", () => {
clearInterval(progressTimer);
progressTimer = null;
// 如果进度条不是 100%,平滑回退
if (percent < 100) {
smoothRevert();
}
});
// 更新进度条和进度文本
function updateProgress(percent) {
const degrees = (percent / 100) * 360;
progress.style.background = `conic-gradient(#3498db 0deg, #3498db ${degrees}deg, transparent ${degrees}deg)`;
progressText.textContent = `${percent}%`;
}
// 平滑回退进度条
function smoothRevert() {
const revertInterval = 10; // 回退动画间隔
const revertStep = 1; // 回退步长
clearTimeout(timer);
const revertTimer = setInterval(() => {
percent -= revertStep;
if (percent <= 0) {
clearInterval(revertTimer);
percent = 0;
updateProgress(percent);
} else {
updateProgress(percent);
}
}, revertInterval);
}
// 完成后的处理逻辑
function complete() {
console.log("开始执行业务逻辑");
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Touch Progress Example</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
#container {
width: 200px;
height: 88px;
border: 1px solid black;
position: relative;
}
#progress {
width: 0%;
height: 100%;
background-color: skyblue;
transition: width 1s linear;
position: absolute;
z-index: 2;
}
#text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 3;
}
</style>
</head>
<body>
<div id="container">
<div id="text">Long Press</div>
<div id="progress"></div>
</div>
<script>
// 获取元素
const container = document.getElementById("container");
const pickupProgress = document.getElementById("progress");
const text = document.getElementById("text");
let touchEventTimer = null;
//这个事件负责
const handleTouchStart = (e) => {
e.preventDefault();
pickupProgress.style.width = "100%";
touchEventTimer = setTimeout(() => {
console.log("开始执行touch完成后的事件逻辑");
touchEventTimer = null;
}, 1000);
console.log("touch开始", touchEventTimer);
};
const handleTouchEnd = () => {
console.log("touch结束");
//未touch 1s 终止计时的任务
if (touchEventTimer) {
pickupProgress.style.width = "0%";
clearTimeout(touchEventTimer);
touchEventTimer = null;
} else {
text.innerText = "Finished";
pickupProgress.style.background = "pink";
}
};
if (container) {
container.addEventListener("touchstart", handleTouchStart, {
passive: false,
});
container.addEventListener("touchend", handleTouchEnd, {
passive: false,
});
}
</script>
</body>
</html>