Files
nba-fan-hub/static/app.js
T

277 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* NBA球迷大全 前端逻辑(原生 JS,无构建) */
const $ = (s) => document.querySelector(s);
const $$ = (s) => [...document.querySelectorAll(s)];
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[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 = `<div class="avatar">${role === "user" ? "🧑" : "🤖"}</div><div class="bubble">${html}</div>`;
$("#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 = `<div class="avatar">🤖</div><div class="bubble"><span class="typing"><i></i><i></i><i></i></span></div>`;
$("#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, "<br>");
if (d.sources && d.sources.length) {
html += "<br>" + d.sources.map((s) => `<span class="src-tag">来源:${esc(s.tool)}</span>`).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) => `<button>${esc(q)}</button>`).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) => `
<div class="card" onclick="openTeam(${t.id})">
<h3>${esc(t.name)} <span class="en">${esc(t.name_en)}</span></h3>
<div class="meta">🏙️ ${esc(t.city)} · ${esc(t.arena)}<br>🏆 总冠军 ×${t.champion_count} · 建队 ${t.founded} 年<br>🧑‍🏫 主帅:${esc(t.head_coach)}</div>
</div>`).join("");
}
async function openTeam(id) {
const d = await getJSON(`/api/teams/${id}`);
const t = d.team;
const roster = d.roster.map((p) => `<div>${esc(p.name)} <span class="en">${esc(p.position)} #${p.number}</span> — 场均 ${p.season.pts}分</div>`).join("");
const games = d.recent_games.map((g) => `<div>${esc(g.game_time.slice(0, 10))} ${esc(g.away_team)} ${g.away_score ?? "?"} : ${g.home_score ?? "?"} ${esc(g.home_team)}${g.status === "finished" ? "已结束" : "未开始"}</div>`).join("");
openModal(`
<h2>${esc(t.name)} <span class="en">${esc(t.name_en)}</span></h2>
<div class="en">${esc(t.city)} · ${esc(t.arena)} · 建队 ${t.founded} · 总冠军 ×${t.champion_count}</div>
<p style="margin-top:8px;font-size:14px;color:#c9d4e0">${esc(t.intro)}</p>
<div class="section-title">🧑‍🏫 主教练</div><div>${esc(t.head_coach)}</div>
<div class="section-title">⭐ 主要球员(本赛季场均)</div>${roster || "暂无"}
<div class="section-title">📅 近期比赛</div>${games || "暂无"}
`);
}
/* ================= 球员 ================= */
async function loadPlayers(q) {
const players = await getJSON(`/api/players?q=${encodeURIComponent(q)}&limit=60`);
$("#grid-players").innerHTML = players.map((p) => `
<div class="card" onclick="openPlayer(${p.id})">
<h3>${esc(p.name)} <span class="en">${esc(p.position)} #${p.number}</span></h3>
<div class="en">${esc(p.team || "")} · ${esc(p.country)}</div>
<div class="meta">本季:<span class="big-num">${p.season.pts}</span> 分 / ${p.season.reb} 板 / ${p.season.ast} 助</div>
</div>`).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(`
<h2>${esc(p.name)} <span class="en">${esc(p.name_en)}</span></h2>
<div class="en">${esc(p.team || "自由球员")} · ${esc(p.position)} · #${p.number} · ${esc(p.country)}</div>
<div class="kv">
<div><span class="k">身高</span><span class="v">${p.height_cm} cm</span></div>
<div><span class="k">体重</span><span class="v">${p.weight_kg} kg</span></div>
<div><span class="k">选秀</span><span class="v">${esc(p.draft)}</span></div>
<div><span class="k">年薪</span><span class="v">$${(p.salary_m / 100).toFixed(2)} 亿</span></div>
<div><span class="k">本季场均</span><span class="v">${p.season.pts}${p.season.reb}${p.season.ast}助</span></div>
<div><span class="k">本季防守</span><span class="v">${p.season.stl}${p.season.blk}帽</span></div>
<div><span class="k">生涯场均</span><span class="v">${p.career.pts}${p.career.reb}${p.career.ast}助</span></div>
<div><span class="k">生涯场次</span><span class="v">${p.career.games} 场</span></div>
</div>
<div class="section-title">🏅 荣誉</div><div style="font-size:13.5px;color:#c9d4e0">${esc(p.awards || "暂无")}</div>
<div class="section-title">📝 简介</div><div style="font-size:13.5px;color:#c9d4e0">${esc(p.bio || "暂无")}</div>
`);
}
/* ================= 比赛 ================= */
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 ? `<span class="score-big">${g.away_score}</span> : <span class="score-big">${g.home_score}</span>` : "VS";
return `<div class="list-item" onclick="openGame(${g.id})">
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:6px">
<span class="tag">${esc(g.round_name)}</span>
<span class="status-pill status-${g.status}">${finished ? "已结束" : g.status === "scheduled" ? "未开始" : "进行中"}</span>
</div>
<div class="score-line" style="margin-top:8px">
<span style="flex:1;text-align:right">${esc(g.away_team)}</span>
<span class="vs">客</span>${score}<span class="vs">主</span>
<span style="flex:1">${esc(g.home_team)}</span>
</div>
<div class="meta">🕐 ${esc(g.game_time)} · ${esc(g.venue)} · ${esc(g.broadcast)}</div>
</div>`;
}).join("") || '<div class="meta">没有符合条件的比赛</div>';
}
$$("#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
? `<div class="score-line" style="font-size:22px;justify-content:center;margin:10px 0">
<span>${esc(g.away_team)}</span> <span class="score-big">${g.away_score}</span> : <span class="score-big">${g.home_score}</span> <span>${esc(g.home_team)}</span></div>`
: `<div class="score-line" style="font-size:20px;justify-content:center;margin:10px 0">${esc(g.away_team)} VS ${esc(g.home_team)}</div>`;
const stats = (g.box_score || []).map((s) => `
<tr><td>${esc(s.team_name)}</td><td>${esc(s.player_name)}</td>
<td class="num">${s.points}</td><td class="num">${s.rebounds}</td><td class="num">${s.assists}</td>
<td class="num">${s.steals}</td><td class="num">${s.blocks}</td><td class="num">${s.minutes}</td></tr>`).join("");
openModal(`
<h2 style="text-align:center">${esc(g.round_name)}</h2>
${head}
<div style="text-align:center;color:var(--sub);font-size:13px">🕐 ${esc(g.game_time)} · ${esc(g.venue)} · ${esc(g.broadcast)}</div>
${stats ? `<div class="section-title">📊 球员技术统计</div>
<table><tr><th>球队</th><th>球员</th><th>得分</th><th>篮板</th><th>助攻</th><th>抢断</th><th>盖帽</th><th>分钟</th></tr>${stats}</table>` : ""}
`);
}
/* ================= 新闻 ================= */
async function loadNews(q) {
const news = await getJSON(`/api/news?q=${encodeURIComponent(q)}&limit=30`);
$("#list-news").innerHTML = news.map((n) => `
<div class="list-item" onclick="openNews(${n.id})">
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<span class="tag">${n.kind === "wiki" ? "百科" : "新闻"}</span>
<h3>${esc(n.title)}</h3>
</div>
<div class="meta">🕐 ${esc(n.publish_time)} · ${esc(n.source)}${n.author ? " · " + esc(n.author) : ""}</div>
<div class="meta" style="margin-top:4px">${esc(n.summary || n.content || "")}…</div>
</div>`).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(`
<h2>${esc(n.title)}</h2>
<div class="en">🕐 ${esc(n.publish_time)} · ${esc(n.source)} · ${esc(n.author || "")} · ${n.kind === "wiki" ? "百科词条" : "新闻"}</div>
<div class="news-body" style="margin-top:12px">${esc(n.content).replace(/\n/g, "<br>")}</div>
`);
}
/* ================= 人物 ================= */
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) => `
<div class="card" onclick="openPerson(${p.id})">
<h3>${esc(p.name)} <span class="en">${esc(p.name_en || "")}</span></h3>
<div class="meta"><span class="tag">${esc(p.role_cn)}</span>${esc(p.title || "")}${p.team ? " · " + esc(p.team) : ""}</div>
<div class="meta">${esc((p.bio || "").slice(0, 60))}…</div>
</div>`).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(`
<h2>${esc(p.name)} <span class="en">${esc(p.name_en || "")}</span></h2>
<div class="en">${esc(p.role_cn)} · ${esc(p.title || "")}${p.team ? " · " + esc(p.team) : ""}</div>
<div class="section-title">📝 简介</div><div style="font-size:14px;line-height:1.8;color:#c9d4e0">${esc(p.bio || "暂无")}</div>
<div class="section-title">🏅 成就</div><div style="font-size:14px;line-height:1.8;color:#c9d4e0">${esc(p.achievements || "暂无")}</div>
`);
}
/* ================= 排名 ================= */
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]) => `
<h3 style="margin:14px 0 8px;color:var(--orange2)">${conf}赛区</h3>
<table><tr><th>排名</th><th>球队</th><th>胜</th><th>负</th><th>胜率</th><th>战绩</th></tr>
${list.map((r) => `<tr><td class="num">${r.rank}</td><td>${esc(r.team)}</td>
<td class="num">${r.wins}</td><td class="num">${r.losses}</td>
<td class="num">${r.win_pct}%</td><td class="num">${r.wins}-${r.losses}</td></tr>`).join("")}
</table>`).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"); });