H5防误触按钮

254 阅读2分钟

前端开发中的防误触按钮的形式:滑动触发事件 长摁触发事件

滑动触发事件

<!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>

20250513154141_rec_.gif

<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>

20250513184614_rec_.gif

<!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>

20250613144059_rec_.gif