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