v1.2.5 管理后台表格点击表头排序:任意字段升序/降序切换(▲▼指示),后端字段白名单校验防注入
This commit is contained in:
@@ -191,7 +191,15 @@ def list_rows(table):
|
|||||||
page = max(1, int(request.args.get("page", 1)))
|
page = max(1, int(request.args.get("page", 1)))
|
||||||
size = min(100, max(1, int(request.args.get("size", 20))))
|
size = min(100, max(1, int(request.args.get("size", 20))))
|
||||||
q = (request.args.get("q") or "").strip()
|
q = (request.args.get("q") or "").strip()
|
||||||
|
# 排序:字段白名单校验(防注入),默认 id 降序
|
||||||
cols = _columns(table)
|
cols = _columns(table)
|
||||||
|
valid_fields = {c["name"] for c in cols}
|
||||||
|
sort = request.args.get("sort", "") or "id"
|
||||||
|
if sort not in valid_fields:
|
||||||
|
sort = "id"
|
||||||
|
order = (request.args.get("order", "") or "desc").lower()
|
||||||
|
if order not in ("asc", "desc"):
|
||||||
|
order = "desc"
|
||||||
where, args = "", []
|
where, args = "", []
|
||||||
if q:
|
if q:
|
||||||
fields = SEARCHABLE.get(table) or ()
|
fields = SEARCHABLE.get(table) or ()
|
||||||
@@ -200,10 +208,11 @@ def list_rows(table):
|
|||||||
where = "WHERE " + " OR ".join(f"{f} LIKE ? ESCAPE '\\'" for f in fields)
|
where = "WHERE " + " OR ".join(f"{f} LIKE ? ESCAPE '\\'" for f in fields)
|
||||||
args = [like] * len(fields)
|
args = [like] * len(fields)
|
||||||
total = query_one(f"SELECT COUNT(*) AS c FROM {table} {where}", args)["c"]
|
total = query_one(f"SELECT COUNT(*) AS c FROM {table} {where}", args)["c"]
|
||||||
rows = query(f"SELECT * FROM {table} {where} ORDER BY id DESC LIMIT ? OFFSET ?",
|
rows = query(f"SELECT * FROM {table} {where} ORDER BY {sort} {order.upper()}, id {order.upper()} LIMIT ? OFFSET ?",
|
||||||
args + [size, (page - 1) * size])
|
args + [size, (page - 1) * size])
|
||||||
return jsonify({"table": table, "cn": TABLES[table], "columns": cols,
|
return jsonify({"table": table, "cn": TABLES[table], "columns": cols,
|
||||||
"total": total, "page": page, "size": size, "rows": rows})
|
"total": total, "page": page, "size": size, "rows": rows,
|
||||||
|
"sort": sort, "order": order})
|
||||||
|
|
||||||
|
|
||||||
def get_row(table, rid):
|
def get_row(table, rid):
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ body { background:var(--bg); color:var(--txt); font-family:"PingFang SC","Micros
|
|||||||
table { width:100%; border-collapse:collapse; }
|
table { width:100%; border-collapse:collapse; }
|
||||||
th, td { padding:9px 11px; text-align:left; font-size:13px; border-bottom:1px solid var(--line); white-space:nowrap; max-width:260px; overflow:hidden; text-overflow:ellipsis; }
|
th, td { padding:9px 11px; text-align:left; font-size:13px; border-bottom:1px solid var(--line); white-space:nowrap; max-width:260px; overflow:hidden; text-overflow:ellipsis; }
|
||||||
th { background:var(--bg2); color:var(--sub); font-weight:600; font-size:12px; position:sticky; top:0; }
|
th { background:var(--bg2); color:var(--sub); font-weight:600; font-size:12px; position:sticky; top:0; }
|
||||||
|
th.sortable { cursor:pointer; user-select:none; transition:.15s; }
|
||||||
|
th.sortable:hover { color:var(--orange2); }
|
||||||
|
th.sort-active { color:var(--orange2); }
|
||||||
tr:hover td { background:rgba(249,115,22,.05); }
|
tr:hover td { background:rgba(249,115,22,.05); }
|
||||||
td.num { text-align:center; }
|
td.num { text-align:center; }
|
||||||
.row-ops { display:flex; gap:6px; }
|
.row-ops { display:flex; gap:6px; }
|
||||||
|
|||||||
+19
-4
@@ -4,7 +4,7 @@ const $$ = (s) => [...document.querySelectorAll(s)];
|
|||||||
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
||||||
|
|
||||||
let TOKEN = localStorage.getItem("nba_admin_token") || "";
|
let TOKEN = localStorage.getItem("nba_admin_token") || "";
|
||||||
let CUR = { table: "", page: 1, q: "" };
|
let CUR = { table: "", page: 1, q: "", sort: "id", order: "desc" };
|
||||||
let LOOKUPS = {}; // teams/players/leagues/sports → {id: name}
|
let LOOKUPS = {}; // teams/players/leagues/sports → {id: name}
|
||||||
|
|
||||||
/* 字段中文名 */
|
/* 字段中文名 */
|
||||||
@@ -79,7 +79,7 @@ function switchPage(p) {
|
|||||||
$("#page-config").classList.toggle("hidden", p !== "config");
|
$("#page-config").classList.toggle("hidden", p !== "config");
|
||||||
if (p === "stats") loadStats();
|
if (p === "stats") loadStats();
|
||||||
else if (p === "config") loadConfig();
|
else if (p === "config") loadConfig();
|
||||||
else { CUR = { table: p, page: 1, q: "" }; $("#tbl-search").value = ""; loadTable(); }
|
else { CUR = { table: p, page: 1, q: "", sort: "id", order: "desc" }; $("#tbl-search").value = ""; loadTable(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================= 仪表盘 ================= */
|
/* ================= 仪表盘 ================= */
|
||||||
@@ -108,11 +108,15 @@ async function loadLookups() {
|
|||||||
|
|
||||||
async function loadTable() {
|
async function loadTable() {
|
||||||
$("#table-title").textContent = TITLE_CN[CUR.table] || CUR.table;
|
$("#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 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 cols = d.columns.filter((c) => c.name !== "created_at");
|
||||||
const rows = d.rows;
|
const rows = d.rows;
|
||||||
if (!Object.keys(LOOKUPS).length) await loadLookups().catch(() => {});
|
if (!Object.keys(LOOKUPS).length) await loadLookups().catch(() => {});
|
||||||
const thead = `<tr>${cols.map((c) => `<th>${esc(FIELD_CN[CUR.table]?.[c.name] || c.name)}</th>`).join("")}<th>操作</th></tr>`;
|
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 tbody = rows.map((r) => {
|
||||||
const tds = cols.map((c) => {
|
const tds = cols.map((c) => {
|
||||||
let v = r[c.name];
|
let v = r[c.name];
|
||||||
@@ -141,6 +145,17 @@ $("#tbl-search-btn").addEventListener("click", () => { CUR.q = $("#tbl-search").
|
|||||||
$("#tbl-search").addEventListener("keydown", (e) => { if (e.key === "Enter") $("#tbl-search-btn").click(); });
|
$("#tbl-search").addEventListener("keydown", (e) => { if (e.key === "Enter") $("#tbl-search-btn").click(); });
|
||||||
$("#tbl-add").addEventListener("click", () => openEdit(CUR.table, null));
|
$("#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 };
|
let EDIT = { table: "", id: null };
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user