(async function () { let myId = null; let myUser = null; const cursors = {}; let showCursors = localStorage.getItem("showCursors") !== "false"; /* ----------------- 获取用户信息 ------------------ */ async function loadUser() { try { const res = await fetch("https://bbs.celemiao.com/api/user", { credentials: "include" }); const data = await res.json(); myUser = data; } catch { myUser = { username: "Guest" }; } } await loadUser(); const socket = new WebSocket("wss://" + window.location.host + "/ws"); /* ----------------- 颜色生成 ------------------ */ function getColorFromId(id) { let hash = 0; for (let i = 0; i < id.length; i++) { hash = id.charCodeAt(i) + ((hash << 5) - hash); } const color = (hash & 0x00FFFFFF) .toString(16) .toUpperCase(); return "#" + "00000".substring(0, 6 - color.length) + color; } /* ----------------- WebSocket ------------------ */ socket.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === "init") { myId = data.id; return; } if (data.type === "disconnect") { if (cursors[data.id]) { cursors[data.id].el.remove(); delete cursors[data.id]; } return; } if (!showCursors) return; if (!cursors[data.id]) { createCursor(data.id, data.username); } cursors[data.id].targetX = data.x; cursors[data.id].targetY = data.y; }; /* ----------------- 创建光标 ------------------ */ function createCursor(id, username) { const color = getColorFromId(id); const wrapper = document.createElement("div"); wrapper.style.position = "fixed"; wrapper.style.pointerEvents = "none"; wrapper.style.zIndex = 9999; wrapper.style.transform = "translate(-2px,-2px)"; const icon = document.createElement("div"); icon.style.width = "24px"; icon.style.height = "24px"; icon.style.background = color; icon.style.clipPath = "polygon(0 0, 0 100%, 35% 65%, 55% 100%, 70% 95%, 50% 60%, 100% 60%)"; const name = document.createElement("div"); name.innerText = username || "User"; name.style.fontSize = "12px"; name.style.color = "white"; name.style.background = color; name.style.padding = "2px 6px"; name.style.borderRadius = "4px"; name.style.marginTop = "2px"; name.style.whiteSpace = "nowrap"; name.style.fontFamily = "sans-serif"; wrapper.appendChild(icon); wrapper.appendChild(name); document.body.appendChild(wrapper); cursors[id] = { el: wrapper, x: 0, y: 0, targetX: 0, targetY: 0 }; } /* ----------------- 平滑动画 ------------------ */ function animate() { for (let id in cursors) { const c = cursors[id]; const lerp = 0.2; c.x += (c.targetX - c.x) * lerp; c.y += (c.targetY - c.y) * lerp; c.el.style.left = c.x + "px"; c.el.style.top = c.y + "px"; } requestAnimationFrame(animate); } requestAnimationFrame(animate); /* ----------------- 鼠标发送 ------------------ */ window.addEventListener("mousemove", throttle((e) => { if (!myId) return; socket.send(JSON.stringify({ x: e.clientX, y: e.clientY, username: myUser ? myUser.username : "Guest" })); }, 30)); function throttle(fn, limit) { let last = 0; return function () { const now = Date.now(); if (now - last >= limit) { fn.apply(this, arguments); last = now; } } } /* ------------------------- UI 开关 ------------------------- */ const button = document.createElement("div"); button.innerText = "🖱"; button.style.position = "fixed"; button.style.bottom = "20px"; button.style.right = "20px"; button.style.width = "40px"; button.style.height = "40px"; button.style.background = "#333"; button.style.color = "white"; button.style.display = "flex"; button.style.alignItems = "center"; button.style.justifyContent = "center"; button.style.borderRadius = "50%"; button.style.cursor = "pointer"; button.style.zIndex = 10000; button.style.boxShadow = "0 2px 8px rgba(0,0,0,0.3)"; document.body.appendChild(button); const panel = document.createElement("div"); panel.style.position = "fixed"; panel.style.bottom = "70px"; panel.style.right = "20px"; panel.style.background = "white"; panel.style.padding = "10px"; panel.style.borderRadius = "8px"; panel.style.boxShadow = "0 4px 10px rgba(0,0,0,0.2)"; panel.style.zIndex = 10000; panel.style.display = "none"; panel.innerHTML = ` `; document.body.appendChild(panel); const toggle = panel.querySelector("#cursorToggle"); toggle.checked = showCursors; toggle.addEventListener("change", () => { showCursors = toggle.checked; localStorage.setItem("showCursors", showCursors); if (!showCursors) { for (let id in cursors) { cursors[id].el.remove(); } } }); button.onclick = () => { panel.style.display = panel.style.display === "none" ? "block" : "none"; }; })();