/* NBA球迷大全 管理后台逻辑(原生 JS) */ const $ = (s) => document.querySelector(s); const $$ = (s) => [...document.querySelectorAll(s)]; const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); let TOKEN = localStorage.getItem("nba_admin_token") || ""; let CUR = { table: "", page: 1, q: "" }; let LOOKUPS = {}; // teams/players/leagues/sports → {id: name} /* 字段中文名 */ const FIELD_CN = { teams: { league_id: "联赛ID", code: "缩写", name: "球队名", name_en: "英文名", city: "城市", arena: "主场球馆", founded: "建队年份", champion_count: "总冠军数", head_coach: "主教练", logo_url: "Logo地址", intro: "简介" }, players: { team_id: "所属球队", name: "姓名", name_en: "英文名", position: "位置", number: "球衣号", height_cm: "身高(cm)", weight_kg: "体重(kg)", birth_date: "出生日期", country: "国家", draft_year: "选秀年份", draft_pick: "选秀顺位(0=落选)", salary_m: "年薪(万美元)", season_pts: "本季得分", season_reb: "本季篮板", season_ast: "本季助攻", season_stl: "本季抢断", season_blk: "本季盖帽", season_min: "本季时间", career_pts: "生涯得分", career_reb: "生涯篮板", career_ast: "生涯助攻", career_games: "生涯场次", career_steals: "生涯抢断", career_blocks: "生涯盖帽", awards: "主要荣誉", bio: "简介" }, games: { league_id: "联赛ID", season: "赛季", round_name: "轮次", game_time: "开赛时间", status: "状态", home_team_id: "主队", away_team_id: "客队", home_score: "主队得分", away_score: "客队得分", venue: "球馆", broadcast: "转播" }, game_player_stats: { game_id: "比赛ID", player_id: "球员", team_id: "球队", points: "得分", rebounds: "篮板", assists: "助攻", steals: "抢断", blocks: "盖帽", minutes: "分钟" }, standings: { league_id: "联赛ID", season: "赛季", team_id: "球队", wins: "胜", losses: "负", win_pct: "胜率", conference: "分区", rank: "排名" }, news: { sport_id: "运动ID", league_id: "联赛ID", team_id: "球队", title: "标题", content: "正文", author: "作者", source: "来源", publish_time: "发布时间", tags: "标签(逗号分隔)", kind: "类型" }, persons: { sport_id: "运动ID", league_id: "联赛ID", team_id: "球队", name: "姓名", name_en: "英文名", role: "角色代码", role_cn: "角色中文", title: "头衔", bio: "简介", achievements: "成就" }, sports: { code: "代码", name: "名称", name_en: "英文名" }, leagues: { sport_id: "运动ID", code: "代码", name: "名称", name_en: "英文名", country: "国家", season: "赛季" }, }; const LONG_TEXT = new Set(["intro", "bio", "awards", "content", "achievements", "tags", "logo_url"]); const ENUM_FIELDS = { games: { status: [["scheduled", "未开始"], ["live", "进行中"], ["finished", "已结束"]] }, news: { kind: [["news", "新闻"], ["wiki", "百科词条"]] }, persons: { role: [["coach", "教练"], ["agent", "经纪人"], ["commentator", "评论员"], ["host", "主持人"], ["gm", "总经理"], ["legend", "传奇"]] }, }; /* ================= 请求 ================= */ async function api(url, opt = {}) { const headers = { "Content-Type": "application/json" }; if (TOKEN) headers["Authorization"] = "Bearer " + TOKEN; const r = await fetch(url, { ...opt, headers }); if (r.status === 401) { const d = await r.json().catch(() => ({})); if (url.includes("/login")) throw new Error(d.error || "密码错误"); logout(); throw new Error(d.error || "登录过期"); } const d = await r.json().catch(() => ({})); if (!r.ok) throw new Error(d.error || `请求失败(${r.status})`); return d; } /* ================= 登录 ================= */ async function doLogin() { const pw = $("#login-pw").value; try { const d = await api("/api/admin/login", { method: "POST", body: JSON.stringify({ password: pw }) }); TOKEN = d.token; localStorage.setItem("nba_admin_token", TOKEN); $("#login-err").textContent = ""; enterMain(); } catch (e) { $("#login-err").textContent = e.message; } } function logout() { TOKEN = ""; localStorage.removeItem("nba_admin_token"); $("#main-view").classList.add("hidden"); $("#login-view").classList.remove("hidden"); } function enterMain() { $("#login-view").classList.add("hidden"); $("#main-view").classList.remove("hidden"); switchPage("stats"); } $("#login-btn").addEventListener("click", doLogin); $("#login-pw").addEventListener("keydown", (e) => { if (e.key === "Enter") doLogin(); }); $("#logout-btn").addEventListener("click", () => { api("/api/admin/logout", { method: "POST" }).catch(() => {}); logout(); }); /* ================= 导航 ================= */ $$(".side nav button[data-page]").forEach((b) => b.addEventListener("click", () => switchPage(b.dataset.page))); function switchPage(p) { $$(".side nav button").forEach((x) => x.classList.toggle("active", x.dataset.page === p)); $("#page-stats").classList.toggle("hidden", p !== "stats"); $("#page-table").classList.toggle("hidden", !["teams", "players", "games", "game_player_stats", "standings", "news", "persons", "sports", "leagues"].includes(p)); $("#page-config").classList.toggle("hidden", p !== "config"); if (p === "stats") loadStats(); else if (p === "config") loadConfig(); else { CUR = { table: p, page: 1, q: "" }; $("#tbl-search").value = ""; loadTable(); } } /* ================= 仪表盘 ================= */ async function loadStats() { const d = await api("/api/admin/stats"); $("#stats-grid").innerHTML = Object.entries(d.tables).map(([k, v]) => `
${v.count}
${esc(v.cn)}
`).join("") + `
${d.config}
配置项
`; } /* ================= 数据表 CRUD ================= */ const TITLE_CN = { teams: "🏟️ 球队管理", players: "⭐ 球员管理", games: "📅 比赛管理", game_player_stats: "📊 比赛技术统计管理", standings: "🏆 排名管理", news: "📰 新闻/百科管理", persons: "👥 人物管理", sports: "🏈 运动项目管理", leagues: "🏀 联赛管理" }; async function loadLookups() { const jobs = {}; jobs.teams = api("/api/teams?limit=50"); jobs.players = api("/api/players?limit=200"); jobs.leagues = api("/api/admin/leagues?size=50").catch(() => ({ rows: [] })); jobs.sports = api("/api/admin/sports?size=10").catch(() => ({ rows: [] })); const [teams, players, leagues, sports] = await Promise.all([jobs.teams, jobs.players, jobs.leagues, jobs.sports]); LOOKUPS.teams = Object.fromEntries(teams.map((t) => [t.id, t.name])); LOOKUPS.players = Object.fromEntries(players.map((p) => [p.id, p.name])); LOOKUPS.leagues = Object.fromEntries((leagues.rows || []).map((l) => [l.id, `${l.name}(${l.code})`])); LOOKUPS.sports = Object.fromEntries((sports.rows || []).map((s) => [s.id, s.name])); } async function loadTable() { $("#table-title").textContent = TITLE_CN[CUR.table] || CUR.table; const d = await api(`/api/admin/${CUR.table}?page=${CUR.page}&size=20&q=${encodeURIComponent(CUR.q)}`); const cols = d.columns.filter((c) => c.name !== "created_at"); const rows = d.rows; if (!Object.keys(LOOKUPS).length) await loadLookups().catch(() => {}); const thead = `${cols.map((c) => `${esc(FIELD_CN[CUR.table]?.[c.name] || c.name)}`).join("")}操作`; const tbody = rows.map((r) => { const tds = cols.map((c) => { let v = r[c.name]; if (v === null || v === undefined) v = ""; if (c.name === "id") v = `#${v}`; else if (["team_id", "home_team_id", "away_team_id"].includes(c.name)) v = (LOOKUPS.teams || {})[v] || v; else if (c.name === "player_id") v = (LOOKUPS.players || {})[v] || v; else if (c.name === "league_id") v = (LOOKUPS.leagues || {})[v] || v; else if (c.name === "sport_id") v = (LOOKUPS.sports || {})[v] || v; else if (c.name === "status") v = ({ scheduled: "未开始", live: "进行中", finished: "已结束" })[v] || v; else if (c.name === "kind") v = v === "wiki" ? "百科" : v === "news" ? "新闻" : v; else if (typeof v === "string" && v.length > 60) v = v.slice(0, 60) + "…"; else if (c.name === "win_pct" && v !== "") v = (v * 100).toFixed(1) + "%"; return `${esc(String(v))}`; }).join(""); return `${tds}
`; }).join(""); $("#tbl-wrap").innerHTML = `${thead}${tbody || ``}
暂无数据
`; const pages = Math.max(1, Math.ceil(d.total / d.size)); $("#tbl-pager").innerHTML = ` 第 ${CUR.page}/${pages} 页 · 共 ${d.total} 条 `; } function goPage(p) { CUR.page = p; loadTable(); } $("#tbl-search-btn").addEventListener("click", () => { CUR.q = $("#tbl-search").value.trim(); CUR.page = 1; loadTable(); }); $("#tbl-search").addEventListener("keydown", (e) => { if (e.key === "Enter") $("#tbl-search-btn").click(); }); $("#tbl-add").addEventListener("click", () => openEdit(CUR.table, null)); /* ================= 编辑弹窗 ================= */ let EDIT = { table: "", id: null }; async function openEdit(table, id) { EDIT = { table, id }; const d = await api(`/api/admin/${table}?size=1`); const cols = d.columns.filter((c) => c.name !== "created_at"); let row = {}; if (id) row = await api(`/api/admin/${table}/${id}`); $("#modal-title").textContent = (id ? "编辑" : "新增") + " · " + (TITLE_CN[table] || table); $("#modal-form").innerHTML = cols.map((c) => { const cn = FIELD_CN[table]?.[c.name] || c.name; let input = ""; if (c.name === "id") input = ``; else { let val = row[c.name] ?? ""; if (val === null) val = ""; const opts = ENUM_FIELDS[table]?.[c.name]; if (opts) { input = ``; } else if (["team_id", "home_team_id", "away_team_id"].includes(c.name)) { const opts2 = Object.entries(LOOKUPS.teams || {}); input = ``; } else if (c.name === "player_id") { const opts3 = Object.entries(LOOKUPS.players || {}); input = ``; } else if (LONG_TEXT.has(c.name)) { input = ``; } else { input = ``; } } return `
${input}
`; }).join(""); $("#modal").classList.remove("hidden"); } $("#modal-close").addEventListener("click", () => $("#modal").classList.add("hidden")); $("#modal-cancel").addEventListener("click", () => $("#modal").classList.add("hidden")); $("#modal").addEventListener("click", (e) => { if (e.target.id === "modal") $("#modal").classList.add("hidden"); }); $("#modal-save").addEventListener("click", async () => { const payload = {}; $$("#modal-form [data-f]").forEach((el) => { payload[el.dataset.f] = el.value; }); try { if (EDIT.id) await api(`/api/admin/${EDIT.table}/${EDIT.id}`, { method: "PUT", body: JSON.stringify(payload) }); else await api(`/api/admin/${EDIT.table}`, { method: "POST", body: JSON.stringify(payload) }); $("#modal").classList.add("hidden"); loadTable(); } catch (e) { alert(e.message); } }); async function delRow(table, id) { if (!confirm(`确认删除 ID=${id} 的记录?此操作不可恢复!`)) return; try { await api(`/api/admin/${table}/${id}`, { method: "DELETE" }); loadTable(); } catch (e) { alert(e.message); } } /* ================= 站点配置 ================= */ async function loadConfig() { const d = await api("/api/admin/config"); $("#cfg-site_name").value = d.site_name || ""; $("#cfg-site_subtitle").value = d.site_subtitle || ""; $("#cfg-welcome_text").value = d.welcome_text || ""; $("#cfg-welcome_hint").value = d.welcome_hint || ""; $("#cfg-footer_text").value = d.footer_text || ""; $("#cfg-admin_password").value = ""; $("#cfg-entity_mark_mode").value = d.entity_mark_mode === "all" ? "all" : "first"; $("#cfg-suggestion_count").value = d.suggestion_count || "3"; try { $("#cfg-suggestions").value = JSON.parse(d.suggestions || "[]").join("\n"); } catch (e) { $("#cfg-suggestions").value = ""; } $("#cfg-msg").textContent = ""; } $("#cfg-save").addEventListener("click", async () => { const payload = { site_name: $("#cfg-site_name").value.trim(), site_subtitle: $("#cfg-site_subtitle").value.trim(), welcome_text: $("#cfg-welcome_text").value, welcome_hint: $("#cfg-welcome_hint").value.trim(), footer_text: $("#cfg-footer_text").value.trim(), suggestions: JSON.stringify($("#cfg-suggestions").value.split("\n").map((s) => s.trim()).filter(Boolean), null, 0), }; const pw = $("#cfg-admin_password").value; if (pw) payload.admin_password = pw; payload.entity_mark_mode = $("#cfg-entity_mark_mode").value; payload.suggestion_count = String(Math.min(6, Math.max(1, parseInt($("#cfg-suggestion_count").value || "3")))); try { await api("/api/admin/config", { method: "PUT", body: JSON.stringify(payload) }); $("#cfg-msg").textContent = "✅ 保存成功,前台刷新后生效"; $("#cfg-msg").style.color = "var(--green)"; $("#cfg-admin_password").value = ""; } catch (e) { $("#cfg-msg").textContent = "❌ " + e.message; $("#cfg-msg").style.color = "var(--red)"; } }); $("#cfg-reset-sug").addEventListener("click", async () => { const d = await api("/api/admin/suggestions/reset", { method: "POST" }); $("#cfg-suggestions").value = d.suggestions.join("\n"); $("#cfg-msg").textContent = "↺ 已恢复默认快捷问题,记得保存"; $("#cfg-msg").style.color = "var(--orange2)"; }); /* ================= 启动 ================= */ (async function init() { if (TOKEN) { try { await api("/api/admin/stats"); enterMain(); } catch (e) { /* 未登录,停留在登录页 */ } } })();