/* 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; /* ================= Markdown 渲染(marked 本地库,先转义防 XSS) ================= */ function md(text) { if (window.marked) { marked.setOptions({ breaks: true, gfm: true }); return marked.parse(esc(text)); } return esc(text).replace(/\n/g, "
"); } /* 实体高亮:先用占位符替换实体区间(避免 markdown 转换后偏移错位),渲染后再恢复为可点击标签 */ function renderMdWithEntities(reply, entities) { let text = reply || ""; const phs = []; if (entities && entities.length) { const sorted = [...entities].sort((a, b) => b.start - a.start); for (const sp of sorted) { if (sp.start < 0 || sp.end > text.length || sp.start >= sp.end) continue; const ph = `\u27E6E${phs.length}\u27E7`; phs.push(sp); text = text.slice(0, sp.start) + ph + text.slice(sp.end); } } let html = md(text); phs.forEach((sp, i) => { const ph = `\u27E6E${i}\u27E7`; if (html.includes(ph)) { const tag = `${esc(sp.name)}`; html = html.split(ph).join(tag); } }); return html; } /* ================= 标签页切换 ================= */ $$(".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") { if ($("#season-select").options.length) { loadStandings(); loadPlayoffs(); } else loadSeasons(); } } /* ================= 对话 ================= */ 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; } /* 实体快速查看卡片 HTML */ function cardHtml(c) { if (c.type === "team") return `
🏟️
${esc(c.name)}${esc(c.city || "")} · 总冠军×${c.champion_count ?? "?"}
`; if (c.type === "player") return `
${esc(c.name)}${esc(c.team || "自由球员")}${c.season ? " · " + (c.season.pts ?? "?") + "分" : ""}
`; if (c.type === "person") return `
👤
${esc(c.name)}${esc(c.role_cn || "")}
`; if (c.type === "game") { const score = c.status === "finished" ? `${c.away_score ?? "?"} : ${c.home_score ?? "?"}` : "VS"; return `
🏀
${esc(c.away_team || "")} ${score} ${esc(c.home_team || "")}${esc(c.round_name || "")} · ${esc((c.game_time || "").slice(0, 16))}
`; } return ""; } 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 = ""; // 需求4:参考资讯(新闻/百科链接),默认折叠,位于回答块上方 if (d.news_refs && d.news_refs.length) { html += `
📰 参考资讯(${d.news_refs.length})
`; } // 需求3:markdown 渲染 + 需求5:实体特殊标记 html += `
${renderMdWithEntities(d.reply, d.entities)}
`; // 需求5:快速查看入口卡片 if (d.cards && d.cards.length) { html += `
${d.cards.map(cardHtml).join("")}
`; } 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(); }); /* 快捷问题:点击 → 自动填入输入框并自动提交(需求2) */ function askQuick(q) { $("#chat-input").value = q; sendChat(q); } /* 聊天区事件委托:快捷语句 / 实体标记 / 新闻链接 / 卡片 */ $("#chat-list").addEventListener("click", (e) => { const q = e.target.closest(".quick-q"); if (q) { e.preventDefault(); askQuick(q.textContent); return; } const ent = e.target.closest(".entity, .ecard"); if (ent && ent.dataset.type) { openEntity(ent.dataset.type, parseInt(ent.dataset.id)); return; } const news = e.target.closest("a[data-news]"); if (news) { e.preventDefault(); openNews(parseInt(news.dataset.news)); return; } }); function openEntity(type, id) { try { if (type === "team") openTeam(id); else if (type === "player") openPlayer(id); else if (type === "person") openPerson(id); else if (type === "game") openGame(id); } catch (e) { openModal(`

⚠️ 打开失败

该${type}记录可能已被删除(ID=${id})。

`); } } /* 启动信息:开场白 + 快捷问题(从配置加载,管理后台可编辑) */ async function loadBoot() { try { const b = await (await fetch("/api/boot")).json(); if (b.welcome_text) $("#welcome-text").innerHTML = md(b.welcome_text); const qs = b.suggestions || []; const hint = (b.welcome_hint || "试试:") + " "; $("#welcome-hint").innerHTML = hint + qs.map((q) => `${esc(q)}`).join(" / "); $("#chips").innerHTML = qs.map((q) => ``).join(""); $$("#chips button").forEach((b2) => b2.addEventListener("click", () => askQuick(b2.textContent))); if (b.site_name) document.title = b.site_name; if (b.footer_text) $("#footer-text").textContent = b.footer_text; } catch (e) {} } loadBoot(); /* ================= 通用请求 ================= */ 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 || "暂无"}
${DL("team", t.id)}
`); } /* ================= 球员 ================= */ 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 || "暂无")}
${DL("player", p.id)}
`); } /* ================= 比赛 ================= */ 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}
球队球员得分篮板助攻抢断盖帽分钟
` : ""}
${DL("game", g.id)}
`); } /* ================= 新闻 ================= */ 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, "
")}
${DL("news", n.id)}
`); } /* ================= 人物 ================= */ 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 || "暂无")}
${DL("person", p.id)}
`); } /* ================= 排名 ================= */ let standingsConf = ""; let standingsSeason = ""; let bracketData = null; async function loadSeasons() { try { const ss = await getJSON("/api/seasons"); if (ss.length) { standingsSeason = ss[0]; // 默认最新赛季 $("#season-select").innerHTML = ss.map((s) => ``).join(""); loadStandings(); loadPlayoffs(); } } catch (e) {} } $("#season-select").addEventListener("change", (e) => { standingsSeason = e.target.value; loadStandings(); loadPlayoffs(); }); async function loadStandings() { const rows = await getJSON(`/api/standings?conf=${encodeURIComponent(standingsConf)}&season=${encodeURIComponent(standingsSeason || "")}`); 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(); })); /* ================= 季后赛对阵图 ================= */ async function loadPlayoffs() { try { bracketData = await getJSON(`/api/playoffs?season=${encodeURIComponent(standingsSeason || "")}`); renderBracket(); } catch (e) { $("#wrap-bracket").innerHTML = `
对阵图加载失败:${esc(e.message)}
`; } } function matchHtml(m) { if (!m) return ""; const gid = (m.games || [])[0]; const row = (side) => `
${side.advance ? "🏆 " : ""}${esc(side.team)}${side.wins}
`; return `
${row(m.home)}${row(m.away)}
`; } function renderBracket() { const r = (bracketData?.rounds) || {}; const first = [...(r.first?.["东部"] || []), ...(r.first?.["西部"] || [])]; const semi = [...(r.semi?.["东部"] || []), ...(r.semi?.["西部"] || [])]; const conf = [r.conf?.["东部"], r.conf?.["西部"]].filter(Boolean); const final = r.final; const col = (title, items) => `
${title}
${items.map(matchHtml).join("")}
`; $("#wrap-bracket").innerHTML = `

🏆 季后赛对阵图

${esc(bracketData?.season || standingsSeason)} 赛季
${col("首轮", first)} ${col("半决赛", semi)} ${col("分区决赛", conf)} ${col("总决赛", final ? [final] : [])}
💡 点击对阵可查看比赛详情 · 胜者显示 🏆
`; } /* ================= 弹窗 ================= */ function openModal(html) { $("#modal-body").innerHTML = html; $("#modal").classList.remove("hidden"); } /* 详情页链接(弹窗底部) */ const DL = (type, id) => `查看完整详情 →`; $("#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"); });