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

265 lines
16 KiB
JavaScript

/* 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]));
let TOKEN = localStorage.getItem("nba_admin_token") || "";
let CUR = { table: "", page: 1, q: "", sort: "id", order: "desc" };
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: "", sort: "id", order: "desc" }; $("#tbl-search").value = ""; loadTable(); }
}
/* ================= 仪表盘 ================= */
async function loadStats() {
const d = await api("/api/admin/stats");
$("#stats-grid").innerHTML = Object.entries(d.tables).map(([k, v]) =>
`<div class="stat-card"><div class="n">${v.count}</div><div class="t">${esc(v.cn)}</div></div>`).join("") +
`<div class="stat-card"><div class="n">${d.config}</div><div class="t">配置项</div></div>`;
}
/* ================= 数据表 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)}&sort=${encodeURIComponent(CUR.sort)}&order=${CUR.order}`);
const cols = d.columns.filter((c) => c.name !== "created_at");
const rows = d.rows;
if (!Object.keys(LOOKUPS).length) await loadLookups().catch(() => {});
const thead = `<tr>${cols.map((c) => {
const active = CUR.sort === c.name;
const arrow = active ? (CUR.order === "asc" ? " ▲" : " ▼") : "";
return `<th class="sortable ${active ? "sort-active" : ""}" data-sort="${c.name}" title="点击排序">${esc(FIELD_CN[CUR.table]?.[c.name] || c.name)}${arrow}</th>`;
}).join("")}<th>操作</th></tr>`;
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; // ID 列纯数值显示
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 `<td title="${esc(String(r[c.name] ?? ""))}">${esc(String(v))}</td>`;
}).join("");
return `<tr>${tds}<td><div class="row-ops"><button class="btn ghost sm" onclick="openEdit('${CUR.table}',${r.id})">编辑</button><button class="btn danger sm" onclick="delRow('${CUR.table}',${r.id})">删除</button></div></td></tr>`;
}).join("");
$("#tbl-wrap").innerHTML = `<table><thead>${thead}</thead><tbody>${tbody || `<tr><td colspan="${cols.length + 1}" style="text-align:center;color:var(--sub)">暂无数据</td></tr>`}</tbody></table>`;
const pages = Math.max(1, Math.ceil(d.total / d.size));
$("#tbl-pager").innerHTML = `<button class="btn ghost sm" ${CUR.page <= 1 ? "disabled" : ""} onclick="goPage(${CUR.page - 1})">上一页</button>
<span>第 ${CUR.page}/${pages} 页 · 共 ${d.total} 条</span>
<button class="btn ghost sm" ${CUR.page >= pages ? "disabled" : ""} onclick="goPage(${CUR.page + 1})">下一页</button>`;
}
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));
/* 表头点击排序:首次点击升序,再点切降序,循环 */
$("#tbl-wrap").addEventListener("click", (e) => {
const th = e.target.closest("th[data-sort]");
if (!th) return;
const f = th.dataset.sort;
if (CUR.sort === f) CUR.order = CUR.order === "asc" ? "desc" : "asc";
else { CUR.sort = f; CUR.order = "asc"; }
CUR.page = 1;
loadTable();
});
/* ================= 编辑弹窗 ================= */
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 = `<input value="${row.id ?? ""}" disabled>`;
else {
let val = row[c.name] ?? "";
if (val === null) val = "";
const opts = ENUM_FIELDS[table]?.[c.name];
if (opts) {
input = `<select data-f="${c.name}">${opts.map(([v, l]) => `<option value="${v}" ${String(val) === v ? "selected" : ""}>${l}</option>`).join("")}</select>`;
} else if (["team_id", "home_team_id", "away_team_id"].includes(c.name)) {
const opts2 = Object.entries(LOOKUPS.teams || {});
input = `<select data-f="${c.name}"><option value="">(无)</option>${opts2.map(([v, l]) => `<option value="${v}" ${String(val) === v ? "selected" : ""}>${esc(l)}</option>`).join("")}</select>`;
} else if (c.name === "player_id") {
const opts3 = Object.entries(LOOKUPS.players || {});
input = `<select data-f="${c.name}"><option value="">(无)</option>${opts3.map(([v, l]) => `<option value="${v}" ${String(val) === v ? "selected" : ""}>${esc(l)}</option>`).join("")}</select>`;
} else if (LONG_TEXT.has(c.name)) {
input = `<textarea data-f="${c.name}" class="${c.name === "content" || c.name === "bio" || c.name === "intro" || c.name === "achievements" ? "full" : ""}">${esc(val)}</textarea>`;
} else {
input = `<input data-f="${c.name}" value="${esc(val)}" type="${c.type === "INTEGER" || c.type === "REAL" ? "number" : "text"}" step="${c.type === "REAL" ? "any" : "1"}">`;
}
}
return `<div class="${c.name === "id" ? "ro" : ""}"><label>${esc(cn)}${c.name === "id" ? "(自动)" : ""}</label>${input}</div>`;
}).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) { /* 未登录,停留在登录页 */ }
}
})();