/* NBA球迷大全 前端逻辑(原生 JS,无构建) */ const $ = (s) => document.querySelector(s); const $$ = (s) => [...document.querySelectorAll(s)]; const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); const chatHistory = []; let chatBusy = false; /* ================= 标签页切换 ================= */ $$(".tab").forEach((t) => t.addEventListener("click", () => { $$(".tab").forEach((x) => x.classList.remove("active")); $$(".view").forEach((x) => x.classList.remove("active")); t.classList.add("active"); $("#view-" + t.dataset.view).classList.add("active"); loadView(t.dataset.view); })); function loadView(v) { if (v === "teams") loadTeams(); else if (v === "players") loadPlayers(""); else if (v === "games") loadGames(""); else if (v === "news") loadNews(""); else if (v === "persons") loadPersons(""); else if (v === "standings") loadStandings(""); } /* ================= 对话 ================= */ function addMsg(role, html) { const div = document.createElement("div"); div.className = `msg ${role}`; div.innerHTML = `
${role === "user" ? "🧑" : "🤖"}
${html}
`; $("#chat-list").appendChild(div); $("#chat-list").scrollTop = $("#chat-list").scrollHeight; return div; } function showTyping() { const div = document.createElement("div"); div.className = "msg bot"; div.innerHTML = `
🤖
`; $("#chat-list").appendChild(div); $("#chat-list").scrollTop = $("#chat-list").scrollHeight; return div; } async function sendChat(text) { if (chatBusy) return; chatBusy = true; $("#send-btn").disabled = true; addMsg("user", esc(text)); const typing = showTyping(); try { const r = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: text, history: chatHistory.slice(-8) }) }); const d = await r.json(); typing.remove(); if (d.error) { addMsg("bot", `⚠️ ${esc(d.error)}`); return; } let html = esc(d.reply).replace(/\n/g, "
"); if (d.sources && d.sources.length) { html += "
" + d.sources.map((s) => `来源:${esc(s.tool)}`).join(""); } addMsg("bot", html); chatHistory.push({ user: text, assistant: d.reply }); if (chatHistory.length > 20) chatHistory.splice(0, chatHistory.length - 20); } catch (e) { typing.remove(); addMsg("bot", "⚠️ 网络异常,请稍后再试。"); } finally { chatBusy = false; $("#send-btn").disabled = false; } } $("#send-btn").addEventListener("click", () => { const v = $("#chat-input").value.trim(); if (v) { $("#chat-input").value = ""; sendChat(v); } }); $("#chat-input").addEventListener("keydown", (e) => { if (e.key === "Enter") $("#send-btn").click(); }); /* 快捷问题 */ async function loadChips() { try { const qs = await (await fetch("/api/suggestions")).json(); $("#chips").innerHTML = qs.map((q) => ``).join(""); $$("#chips button").forEach((b) => b.addEventListener("click", () => sendChat(b.textContent))); } catch (e) {} } loadChips(); /* ================= 通用请求 ================= */ async function getJSON(url) { const r = await fetch(url); if (!r.ok) throw new Error(r.status); return r.json(); } /* ================= 球队 ================= */ async function loadTeams() { const teams = await getJSON("/api/teams?limit=50"); $("#grid-teams").innerHTML = teams.map((t) => `

${esc(t.name)} ${esc(t.name_en)}

🏙️ ${esc(t.city)} · ${esc(t.arena)}
🏆 总冠军 ×${t.champion_count} · 建队 ${t.founded} 年
🧑‍🏫 主帅:${esc(t.head_coach)}
`).join(""); } async function openTeam(id) { const d = await getJSON(`/api/teams/${id}`); const t = d.team; const roster = d.roster.map((p) => `
${esc(p.name)} ${esc(p.position)} #${p.number} — 场均 ${p.season.pts}分
`).join(""); const games = d.recent_games.map((g) => `
${esc(g.game_time.slice(0, 10))} ${esc(g.away_team)} ${g.away_score ?? "?"} : ${g.home_score ?? "?"} ${esc(g.home_team)}(${g.status === "finished" ? "已结束" : "未开始"})
`).join(""); openModal(`

${esc(t.name)} ${esc(t.name_en)}

${esc(t.city)} · ${esc(t.arena)} · 建队 ${t.founded} · 总冠军 ×${t.champion_count}

${esc(t.intro)}

🧑‍🏫 主教练
${esc(t.head_coach)}
⭐ 主要球员(本赛季场均)
${roster || "暂无"}
📅 近期比赛
${games || "暂无"} `); } /* ================= 球员 ================= */ async function loadPlayers(q) { const players = await getJSON(`/api/players?q=${encodeURIComponent(q)}&limit=60`); $("#grid-players").innerHTML = players.map((p) => `

${esc(p.name)} ${esc(p.position)} #${p.number}

${esc(p.team || "")} · ${esc(p.country)}
本季:${p.season.pts} 分 / ${p.season.reb} 板 / ${p.season.ast} 助
`).join(""); } $("#btn-players").addEventListener("click", () => loadPlayers($("#search-players").value.trim())); $("#search-players").addEventListener("keydown", (e) => { if (e.key === "Enter") $("#btn-players").click(); }); async function openPlayer(id) { const p = await getJSON(`/api/players/${id}`); openModal(`

${esc(p.name)} ${esc(p.name_en)}

${esc(p.team || "自由球员")} · ${esc(p.position)} · #${p.number} · ${esc(p.country)}
身高${p.height_cm} cm
体重${p.weight_kg} kg
选秀${esc(p.draft)}
年薪$${(p.salary_m / 100).toFixed(2)} 亿
本季场均${p.season.pts}分 ${p.season.reb}板 ${p.season.ast}助
本季防守${p.season.stl}断 ${p.season.blk}帽
生涯场均${p.career.pts}分 ${p.career.reb}板 ${p.career.ast}助
生涯场次${p.career.games} 场
🏅 荣誉
${esc(p.awards || "暂无")}
📝 简介
${esc(p.bio || "暂无")}
`); } /* ================= 比赛 ================= */ let gameStatusFilter = ""; async function loadGames(q) { const url = `/api/games?q=${encodeURIComponent(q)}&limit=40`; const games = await getJSON(url); const list = games.filter((g) => !gameStatusFilter || g.status === gameStatusFilter); $("#list-games").innerHTML = list.map((g) => { const finished = g.status === "finished"; const score = finished ? `${g.away_score} : ${g.home_score}` : "VS"; return `
${esc(g.round_name)} ${finished ? "已结束" : g.status === "scheduled" ? "未开始" : "进行中"}
${esc(g.away_team)} ${score} ${esc(g.home_team)}
🕐 ${esc(g.game_time)} · ${esc(g.venue)} · ${esc(g.broadcast)}
`; }).join("") || '
没有符合条件的比赛
'; } $$("#view-games .btn.small").forEach((b) => b.addEventListener("click", () => { $$("#view-games .btn.small").forEach((x) => x.classList.remove("active")); b.classList.add("active"); gameStatusFilter = b.dataset.status; loadGames($("#search-games").value.trim()); })); $("#btn-games").addEventListener("click", () => loadGames($("#search-games").value.trim())); $("#search-games").addEventListener("keydown", (e) => { if (e.key === "Enter") $("#btn-games").click(); }); async function openGame(id) { const g = await getJSON(`/api/games/${id}`); const finished = g.status === "finished"; const head = finished ? `
${esc(g.away_team)} ${g.away_score} : ${g.home_score} ${esc(g.home_team)}
` : `
${esc(g.away_team)} VS ${esc(g.home_team)}
`; const stats = (g.box_score || []).map((s) => ` ${esc(s.team_name)}${esc(s.player_name)} ${s.points}${s.rebounds}${s.assists} ${s.steals}${s.blocks}${s.minutes}`).join(""); openModal(`

${esc(g.round_name)}

${head}
🕐 ${esc(g.game_time)} · ${esc(g.venue)} · ${esc(g.broadcast)}
${stats ? `
📊 球员技术统计
${stats}
球队球员得分篮板助攻抢断盖帽分钟
` : ""} `); } /* ================= 新闻 ================= */ async function loadNews(q) { const news = await getJSON(`/api/news?q=${encodeURIComponent(q)}&limit=30`); $("#list-news").innerHTML = news.map((n) => `
${n.kind === "wiki" ? "百科" : "新闻"}

${esc(n.title)}

🕐 ${esc(n.publish_time)} · ${esc(n.source)}${n.author ? " · " + esc(n.author) : ""}
${esc(n.summary || n.content || "")}…
`).join(""); } $("#btn-news").addEventListener("click", () => loadNews($("#search-news").value.trim())); $("#search-news").addEventListener("keydown", (e) => { if (e.key === "Enter") $("#btn-news").click(); }); async function openNews(id) { const n = await getJSON(`/api/news/${id}`); openModal(`

${esc(n.title)}

🕐 ${esc(n.publish_time)} · ${esc(n.source)} · ${esc(n.author || "")} · ${n.kind === "wiki" ? "百科词条" : "新闻"}
${esc(n.content).replace(/\n/g, "
")}
`); } /* ================= 人物 ================= */ let personRoleFilter = ""; async function loadPersons(q) { const persons = await getJSON(`/api/persons?q=${encodeURIComponent(q)}&limit=60`); const list = persons.filter((p) => !personRoleFilter || p.role === personRoleFilter); $("#grid-persons").innerHTML = list.map((p) => `

${esc(p.name)} ${esc(p.name_en || "")}

${esc(p.role_cn)}${esc(p.title || "")}${p.team ? " · " + esc(p.team) : ""}
${esc((p.bio || "").slice(0, 60))}…
`).join(""); } $$("#view-persons .btn.small").forEach((b) => b.addEventListener("click", () => { $$("#view-persons .btn.small").forEach((x) => x.classList.remove("active")); b.classList.add("active"); personRoleFilter = b.dataset.role; loadPersons($("#search-persons").value.trim()); })); $("#btn-persons").addEventListener("click", () => loadPersons($("#search-persons").value.trim())); $("#search-persons").addEventListener("keydown", (e) => { if (e.key === "Enter") $("#btn-persons").click(); }); async function openPerson(id) { const p = await getJSON(`/api/persons/${id}`); openModal(`

${esc(p.name)} ${esc(p.name_en || "")}

${esc(p.role_cn)} · ${esc(p.title || "")}${p.team ? " · " + esc(p.team) : ""}
📝 简介
${esc(p.bio || "暂无")}
🏅 成就
${esc(p.achievements || "暂无")}
`); } /* ================= 排名 ================= */ let standingsConf = ""; async function loadStandings() { const rows = await getJSON(`/api/standings?conf=${encodeURIComponent(standingsConf)}`); const groups = {}; rows.forEach((r) => { (groups[r.conference] = groups[r.conference] || []).push(r); }); $("#wrap-standings").innerHTML = Object.entries(groups).map(([conf, list]) => `

${conf}赛区

${list.map((r) => ``).join("")}
排名球队胜率战绩
${r.rank}${esc(r.team)} ${r.wins}${r.losses} ${r.win_pct}%${r.wins}-${r.losses}
`).join(""); } $$("#view-standings .btn.small").forEach((b) => b.addEventListener("click", () => { $$("#view-standings .btn.small").forEach((x) => x.classList.remove("active")); b.classList.add("active"); standingsConf = b.dataset.conf; loadStandings(); })); /* ================= 弹窗 ================= */ function openModal(html) { $("#modal-body").innerHTML = html; $("#modal").classList.remove("hidden"); } $("#modal-close").addEventListener("click", () => $("#modal").classList.add("hidden")); $("#modal").addEventListener("click", (e) => { if (e.target.id === "modal") $("#modal").classList.add("hidden"); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape") $("#modal").classList.add("hidden"); });