H

Hou Mengwei

DIGITAL SPACE

Html+CSS|hover效果菜单自动关联最新文章

目前的Hover菜单,自动关联最新文章
可以用 WP REST API 在前端用 JS 拉取最新文章 → 动态生成菜单
第一步,HTML:只保留一个容器(不用手写标题了)

<section class="hover-menu" aria-label="Latest posts hover menu">
  <div id="hm-list" class="hover-menu__wrap"></div>
  <div id="hm-cursor" class="hm-cursor" aria-hidden="true"></div>
</section>

JS:抓取最新 5 篇文章 + 自动填标题/说明/图片/链接

<script>
(async () => {
  const listEl = document.getElementById("hm-list");
  const cursor = document.getElementById("hm-cursor");
  if (!listEl || !cursor) return;

  // 显示几篇(目前为5篇)
  const PER_PAGE = 5;

  // 抓取最新文章(_embed 用来拿特色图/分类名)
  const api = `/wp-json/wp/v2/posts?per_page=${PER_PAGE}&orderby=date&order=desc&_embed=1`;

  let posts = [];
  try {
    const res = await fetch(api, { credentials: "same-origin" });
    if (!res.ok) throw new Error("REST API request failed");
    posts = await res.json();
  } catch (e) {
    console.warn(e);
    listEl.innerHTML = `<div style="padding:16px 6px;">无法加载最新文章(请确认未禁用 REST API)。</div>`;
    return;
  }

  // 生成菜单 HTML(标题 + 说明 + SVG箭头 + data-image)
  listEl.innerHTML = posts.map(p => {
    const title = stripHTML(p.title?.rendered || "");
    const link  = p.link || "#";

    // 说明文字:优先用摘要(excerpt),没有就用日期
    const excerpt = stripHTML(p.excerpt?.rendered || "").trim();
    const desc = excerpt ? clampText(excerpt, 70) : formatDate(p.date);

    // 分类名:取第一个分类(需要 _embed)
    const catName = getFirstCategoryName(p);

    // 特色图:需要 _embed,拿 source_url
    const imgUrl = getFeaturedImageUrl(p) || "";

    // 也可以把 desc 改成 `${catName} · ${formatDate(p.date)}`
    const descLine = catName ? `${catName} · ${desc}` : desc;

    return `
      <a class="hm-item" href="${escapeAttr(link)}" data-image="${escapeAttr(imgUrl)}">
        <div class="hm-curtain" aria-hidden="true"></div>
        <div class="hm-content">
          <div class="hm-title">${escapeHTML(title)}</div>
          <div class="hm-desc">${escapeHTML(descLine)}</div>
        </div>
        <div class="hm-arrow" aria-hidden="true">
          <svg class="hm-arrow__svg" viewBox="0 0 24 24" fill="none">
            <path d="M7 17L17 7" />
            <path d="M9 7h8v8" />
          </svg>
        </div>
      </a>
    `;
  }).join("");

  // ========== 以下为“鼠标跟随预览图”逻辑(自动绑定新生成的 .hm-item) ==========
  const items = listEl.querySelectorAll(".hm-item");

  // 预加载图片
  items.forEach(a => {
    const url = a.getAttribute("data-image");
    if (url) { const img = new Image(); img.src = url; }
  });

  const CURSOR_W = 320, CURSOR_H = 220, PAD = 24, SHIFT_X = 360, LERP = 0.12;
  let mouseX = 0, mouseY = 0, x = 0, y = 0, hovering = false, activeSrc = null, rafId = null;

  document.addEventListener("mousemove", (e) => { mouseX = e.clientX; mouseY = e.clientY; }, { passive: true });

  items.forEach(item => {
    item.addEventListener("mouseenter", () => {
      hovering = true;
      cursor.style.opacity = "1";
      const url = item.getAttribute("data-image");
      if (url) swapImage(url);
      start();
    });

    item.addEventListener("mouseleave", () => {
      hovering = false;
      cursor.style.opacity = "0";
    });
  });

  function start(){ if (!rafId) rafId = requestAnimationFrame(tick); }

  function tick(){
    let tx = mouseX + SHIFT_X;
    let ty = mouseY - CURSOR_H / 2;

    const maxLeft = window.innerWidth - CURSOR_W - PAD;
    const maxTop  = window.innerHeight - CURSOR_H - PAD;

    tx = Math.max(PAD, Math.min(tx, maxLeft));
    ty = Math.max(PAD, Math.min(ty, maxTop));

    x += (tx - x) * LERP;
    y += (ty - y) * LERP;

    const scale = hovering ? 1 : 0.92;
    cursor.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`;

    rafId = requestAnimationFrame(tick);
  }

  function swapImage(url){
    if (activeSrc === url) return;
    activeSrc = url;

    const img = document.createElement("img");
    img.src = url;
    img.alt = "";
    img.className = "hm-img-enter";

    [...cursor.children].forEach(old => {
      old.classList.remove("hm-img-enter");
      old.classList.add("hm-img-exit");
      setTimeout(() => old.remove(), 700);
    });

    cursor.appendChild(img);
  }

  // ======= helpers =======
  function getFeaturedImageUrl(post){
    const fm = post?._embedded?.["wp:featuredmedia"]?.[0];
    return fm?.source_url || fm?.media_details?.sizes?.medium?.source_url || "";
  }

  function getFirstCategoryName(post){
    // WP 会在 _embedded 里给 term;分类通常在 wp:term 的某个数组里
    const termGroups = post?._embedded?.["wp:term"];
    if (!Array.isArray(termGroups)) return "";
    for (const group of termGroups) {
      if (!Array.isArray(group)) continue;
      const cat = group.find(t => t?.taxonomy === "category");
      if (cat?.name) return cat.name;
    }
    return "";
  }

  function stripHTML(html){ return String(html).replace(/<[^>]*>/g, ""); }
  function clampText(s, n){ return s.length > n ? s.slice(0, n-1) + "…" : s; }

  function formatDate(iso){
    try{
      const d = new Date(iso);
      return d.toLocaleDateString("ja-JP", { year:"numeric", month:"2-digit", day:"2-digit" });
    }catch{ return ""; }
  }

  function escapeHTML(s){
    return String(s)
      .replaceAll("&","&").replaceAll("<","<").replaceAll(">",">")
      .replaceAll('"',""").replaceAll("'","'");
  }
  function escapeAttr(s){ return escapeHTML(s); }
})();
</script>

完成。

筛选分类:如只要抓取最新文章中的某个分类

可按分类 ID 只取该分类的最新文章
原文:

const api = `/wp-json/wp/v2/posts?per_page=${PER_PAGE}&orderby=date&order=desc&_embed=1`;

修改为:

const CATEGORY_ID = 123; // ←换成分类ID
const api = `/wp-json/wp/v2/posts?per_page=${PER_PAGE}&orderby=date&order=desc&categories=${CATEGORY_ID}&_embed=1`;

categories= 是 WP REST API 的官方过滤参数,最稳定。
查分类 ID:
在后台打开该分类的编辑页面,浏览器地址栏通常会有 tag_ID=123 或 taxonomy=category&tag_ID=123,这个数字就是分类 ID。