html + css + js 实现卡片轮播

作者:user 发布日期:2025年12月24日 23:04 浏览量:14
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>轮播卡片</title>
    <style>
      body {
        background: #0e0e10;
        color: var(--primary-text);
      }
      section {
        max-width: 1400px;
        margin: 0 auto;
        padding: 6rem 3rem;
      }

      .section-header {
        text-align: center;
        margin-bottom: 4rem;
      }

      .section-tag {
        font-size: 0.875rem;
        letter-spacing: 0.15em;
        text-transform: uppercase;
        color: var(--accent-cyan);
        font-weight: 600;
        margin-bottom: 1rem;
      }

      .section-title {
        font-size: 2.8rem;
        font-weight: 700;
        letter-spacing: 0.03em;
        margin-bottom: 1rem;
        color: #e0bb7d;
      }

      .section-subtitle {
        font-size: 1rem;
        color: var(--text-gray);
        letter-spacing: 0.03em;
        max-width: 600px;
        margin: 0 auto;
      }
      .categories-grid {
        display: grid;
        grid-template-columns: repeat(5, 1fr);
        gap: 1.5rem;
        margin-bottom: 5rem;
      }

      .category-card {
        padding: 2rem 1rem;
        text-align: center;
        border: 1px solid var(--border-gray);
        cursor: pointer;
        transition: var(--transition);
        opacity: 0;
        transform: translateY(2rem);
      }

      .category-card.visible {
        opacity: 1;
        transform: translateY(0);
      }

      .category-card:hover {
        border-color: var(--accent-cyan);
        box-shadow: 0 0 20px rgba(0, 242, 255, 0.1);
      }

      .category-card i {
        font-size: 2.5rem;
        color: var(--primary-text);
        margin-bottom: 1rem;
        display: block;
        transition: var(--transition);
      }

      .category-card:hover i {
        color: var(--accent-cyan);
        transform: scale(1.1);
      }

      .category-card p {
        font-size: 1rem;
        font-weight: 600;
        letter-spacing: 0.03em;
      }

      #platform {
        overflow: hidden;
        padding: 6rem 0; /* 左右铺满,上下留白 */
      }

      .carousel-viewport {
        width: 100%;
        overflow: hidden;
        cursor: grab;
        position: relative;
        user-select: none; /* 防止拖拽时选中文字 */
      }

      .carousel-viewport:active {
        cursor: grabbing;
      }

      .carousel-track {
        display: flex;
        padding: 20px 0;
      }

      .category-card-slide {
        /* 核心:4个卡片一行,减去间隙 */
        flex: 0 0 25%;
        padding: 0 15px;
        box-sizing: border-box;
      }

      .card-content {
        aspect-ratio: 3 / 4; /* 严格执行4:3 */
        background: var(--card-bg);
        border: 1px solid var(--border-gray);
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        transition: var(--transition);
        border-radius: 15px; /* 轻微圆角增加高级感 */
      }

      .card-content i {
        font-size: 3.2rem;
        color: var(--primary-text);
        margin-bottom: 1.2rem;
        transition: var(--transition);
      }

      .card-content p {
        font-size: 1.1rem;
        font-weight: 500;
        color: var(--primary-text);
      }

      /* 悬浮交互 */
      .card-content:hover {
        border-color: #e0bb7d;
        background: rgba(224, 187, 125, 0.05);
        box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
        transform: translateY(-5px);
      }

      .card-content:hover i {
        color: #e0bb7d;
        transform: scale(1.1);
      }

      /* 指示点 */
      .carousel-dots {
        display: flex;
        justify-content: center;
        gap: 12px;
        margin-top: 2.5rem;
      }

      .carousel-viewport {
        user-select: none;
        -webkit-user-drag: none;
      }

      .dot {
        width: 6px;
        height: 6px;
        background: #333;
        border-radius: 50%;
        cursor: pointer;
        transition: all 0.4s ease;
      }

      .dot.active {
        width: 20px;
        border-radius: 10px;
        background: #e0bb7d;
      }

      /* 移动端适配 */
      @media (max-width: 1024px) {
        .category-card-slide {
          flex: 0 0 33.333%;
        }
      }

      @media (max-width: 768px) {
        .category-card-slide {
          flex: 0 0 50%;
        }
        .card-content i {
          font-size: 2.5rem;
        }
      }
    </style>
  </head>
  <body>
    <section id="platform">
      <div class="section-header">
        <div class="section-tag">Platform</div>
        <h2 class="section-title">筑能平台</h2>
        <p class="section-subtitle">
          按建筑类型快速进入场景 (支持鼠标左右滑动)
        </p>
      </div>

      <div class="carousel-viewport" id="carouselViewport">
        <div class="carousel-track" id="categoriesTrack">
          <!-- 由 JavaScript 填充 -->
        </div>
      </div>

      <div class="carousel-dots" id="carouselDots"></div>
    </section>
    <script>
      const categories = [
        {
          title: "写字楼",
          icon: "fas fa-building",
          url: "./list.html?type=workplace",
          backgroundImage: "./images/cases/szsws.png",
        },
        {
          title: "工厂",
          icon: "fas fa-industry",
          url: "./list.html?type=factory",
          backgroundImage: "./images/banners/scene/factory.png",
        },
        {
          title: "学校",
          icon: "fas fa-school",
          url: "./list.html?type=school",
          backgroundImage: "./images/cases/syxx.png",
        },
        {
          title: "医院",
          icon: "fas fa-hospital",
          url: "./list.html?type=hospital",
          backgroundImage: "./images/banners/scene/hospital.png",
        },
        {
          title: "其他",
          icon: "fas fa-ellipsis-h",
          url: "./list.html?type=other",
          backgroundImage: "./images/banners/scene/other.png",
        }, // 也可以跳回内部详情页
      ];
      // ============================================================
      // 2. 筑能平台:彻底解决回弹的轮播逻辑
      // ============================================================

      /**
       * 2.1 获取轮播相关DOM元素
       */
      const viewport = document.getElementById("carouselViewport");
      const track = document.getElementById("categoriesTrack");
      const dotsContainer = document.getElementById("carouselDots");

      /**
       * 2.2 轮播配置变量
       */
      let items = categories;
      const itemsToShow = 4; // 页面可见数量
      let currentIndex = itemsToShow;
      let isDragging = false;
      let isMoving = false;
      let startX = 0;
      let dragStartX = 0;
      let timer = null;

      /**
       * 2.3 初始化轮播
       * - 克隆首尾节点实现无缝循环
       * - 创建指示点
       * - 设置初始位置
       * - 启动自动轮播定时器
       */
      function initCarousel() {
        // 克隆节点
        const headClones = items.slice(0, itemsToShow);
        const tailClones = items.slice(-itemsToShow);
        const combinedItems = [...tailClones, ...items, ...headClones];

        // 清空并重新填充轨道
        track.innerHTML = "";
        combinedItems.forEach((c) => {
          const card = document.createElement("div");
          card.className = "category-card-slide";
          card.innerHTML = `
                      <div class="card-content" style="background-image: url(${c.backgroundImage}); background-size: cover; background-position: center;">
                          <i class="${c.icon}" style="display: none;"></i>
                          <p style="font-size: 30px; font-weight: 600; margin-top: 330px; margin-bottom: 0; color: #fff; text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);">${c.title}</p>
                      </div>
                  `;

          card.onclick = (e) => {
            if (isMoving) {
              e.preventDefault();
              e.stopPropagation();
              return;
            }
            location.href = c.url;
          };

          track.appendChild(card);
        });

        // 创建指示点
        dotsContainer.innerHTML = "";
        items.forEach((_, i) => {
          const dot = document.createElement("div");
          dot.className = `dot ${i === 0 ? "active" : ""}`;
          dot.onclick = () => {
            currentIndex = i + itemsToShow;
            updatePosition(true);
          };
          dotsContainer.appendChild(dot);
        });

        // 设置初始位置
        updatePosition(false);

        // 启动自动轮播
        startTimer();
      }

      /**
       * 2.4 更新轮播位置
       * @param {boolean} animation - 是否启用过渡动画
       */
      function updatePosition(animation = true) {
        const cardWidth = viewport.offsetWidth / itemsToShow; // 计算单张卡片宽度
        track.style.transition = animation
          ? "transform 0.5s cubic-bezier(0.2, 1, 0.3, 1)"
          : "none";
        track.style.transform = `translateX(${-currentIndex * cardWidth}px)`;

        // 更新指示点状态
        updateDots();
      }

      /**
       * 2.5 更新指示点激活状态
       */
      function updateDots() {
        const dots = document.querySelectorAll(".dot");
        if (dots.length === 0) return;

        const logicIndex =
          (currentIndex - itemsToShow + items.length) % items.length;
        dots.forEach((dot, i) => {
          dot.classList.toggle("active", i === logicIndex);
        });
      }

      /**
       * 2.6 获取事件中的X坐标(支持鼠标和触摸事件)
       * @param {Event} e - 事件对象
       * @returns {number} - X坐标
       */
      function getPosX(e) {
        return e.type.includes("mouse") ? e.pageX : e.touches[0].clientX;
      }

      /**
       * 2.7 拖拽开始事件处理
       * @param {Event} e - 事件对象
       */
      function onDragStart(e) {
        isDragging = true;
        isMoving = false;
        startX = getPosX(e);

        // 清除自动轮播定时器
        clearInterval(timer);
        track.style.transition = "none";

        // 获取当前轨道的平移量
        const style = window.getComputedStyle(track);
        const matrix = new WebKitCSSMatrix(style.transform);
        dragStartX = matrix.m41;
      }

      /**
       * 2.8 拖拽移动事件处理
       * @param {Event} e - 事件对象
       */
      function onDragMove(e) {
        if (!isDragging) return;

        const currentX = getPosX(e);
        const diff = currentX - startX;

        // 灵敏度判断:移动超过5像素视为有效移动
        if (Math.abs(diff) > 5) {
          isMoving = true;
        }

        // 实时更新轨道位置
        track.style.transform = `translateX(${dragStartX + diff}px)`;
      }

      /**
       * 2.9 拖拽结束事件处理
       * @param {Event} e - 事件对象
       */
      function onDragEnd(e) {
        if (!isDragging) return;
        isDragging = false;

        const endX = e.type.includes("touch")
          ? e.changedTouches[0].clientX
          : e.pageX;
        const diff = endX - startX;

        // 彻底防回弹逻辑
        const threshold = 40; // 切换阈值:40像素

        if (diff < -threshold) {
          currentIndex++; // 向左划,显示下一张
        } else if (diff > threshold) {
          currentIndex--; // 向右划,显示上一张
        }

        // 强制对齐到正确位置
        updatePosition(true);

        // 延迟重置移动状态
        setTimeout(() => {
          isMoving = false;
        }, 50);

        // 重新启动自动轮播
        startTimer();
      }

      /**
       * 2.10 无缝循环检测(过渡动画结束后)
       */
      track.addEventListener("transitionend", () => {
        // 向右滚动到头时,跳转到开头克隆节点
        if (currentIndex >= items.length + itemsToShow) {
          currentIndex = itemsToShow;
          updatePosition(false);
        }
        // 向左滚动到头时,跳转到末尾克隆节点
        else if (currentIndex < itemsToShow) {
          currentIndex = items.length + itemsToShow - 1;
          updatePosition(false);
        }
      });

      /**
       * 2.11 启动自动轮播定时器
       */
      function startTimer() {
        clearInterval(timer);
        timer = setInterval(() => {
          currentIndex++;
          updatePosition(true);
        }, 3000);
      }

      /**
       * 2.12 绑定轮播事件监听器
       */
      // 拖拽开始
      viewport.addEventListener("mousedown", onDragStart);
      viewport.addEventListener("touchstart", onDragStart, { passive: true });

      // 拖拽移动
      window.addEventListener("mousemove", onDragMove);
      window.addEventListener("touchmove", onDragMove, { passive: false });

      // 拖拽结束
      window.addEventListener("mouseup", onDragEnd);
      window.addEventListener("touchend", onDragEnd);

      /**
       * 2.13 初始化轮播并监听窗口大小变化
       */
      initCarousel();
      window.addEventListener("resize", () => {
        updatePosition(false);
      });
    </script>
  </body>
</html>
已经是最后一篇了!