feat(admin): pagination для users-таблицы (50/стр)
- adminGetUsers возвращает { users, total, page, limit }
- pagination-controls (← 1 … N →) с ellipsis для длинных списков
- shop/gam search callers адаптированы под новый формат ответа
- helper _renderPgnControls переиспользуем для sessions/shop
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1117,6 +1117,7 @@
|
|||||||
<tbody id="users-body"><tr><td colspan="7"><div class="spinner"></div></td></tr></tbody>
|
<tbody id="users-body"><tr><td colspan="7"><div class="spinner"></div></td></tr></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="users-pagination" class="pgn-bar" style="display:none"></div>
|
||||||
<div class="user-panel" id="user-panel">
|
<div class="user-panel" id="user-panel">
|
||||||
<div class="user-panel-header">
|
<div class="user-panel-header">
|
||||||
<div><div class="user-panel-name" id="up-name"></div><div class="user-panel-email" id="up-email"></div></div>
|
<div><div class="user-panel-name" id="up-name"></div><div class="user-panel-email" id="up-email"></div></div>
|
||||||
|
|||||||
@@ -653,11 +653,63 @@
|
|||||||
/* ════════════════════════════════════════════════
|
/* ════════════════════════════════════════════════
|
||||||
ПОЛЬЗОВАТЕЛИ
|
ПОЛЬЗОВАТЕЛИ
|
||||||
════════════════════════════════════════════════ */
|
════════════════════════════════════════════════ */
|
||||||
async function loadUsers() {
|
let _usersPage = 1;
|
||||||
|
const _USERS_PER_PAGE = 50;
|
||||||
|
|
||||||
|
function _ensurePgnStyles() {
|
||||||
|
if (document.getElementById('pgn-bar-style')) return;
|
||||||
|
const s = document.createElement('style');
|
||||||
|
s.id = 'pgn-bar-style';
|
||||||
|
s.textContent = `
|
||||||
|
.pgn-bar { display:flex; align-items:center; justify-content:space-between; gap:10px; padding:14px 4px 4px; font-size:0.85rem; color:var(--text-3); }
|
||||||
|
.pgn-info { font-weight:600; }
|
||||||
|
.pgn-ctrls { display:flex; align-items:center; gap:4px; }
|
||||||
|
.pgn-btn { min-width:32px; height:32px; padding:0 10px; border:1px solid var(--border); background:var(--surface); border-radius:8px; cursor:pointer; font-weight:600; font-family:inherit; font-size:0.85rem; color:var(--text-2); transition:background .12s, color .12s, border-color .12s; }
|
||||||
|
.pgn-btn:hover:not(:disabled) { background:rgba(155,93,229,.08); color:var(--violet); border-color:rgba(155,93,229,.3); }
|
||||||
|
.pgn-btn.active { background:var(--violet); color:#fff; border-color:var(--violet); }
|
||||||
|
.pgn-btn:disabled { opacity:.4; cursor:not-allowed; }
|
||||||
|
.pgn-ellip { padding:0 6px; color:var(--text-3); }
|
||||||
|
`;
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _renderPgnControls(elId, page, total, perPage, gotoFn) {
|
||||||
|
const bar = document.getElementById(elId);
|
||||||
|
if (!bar) return;
|
||||||
|
const pages = Math.max(1, Math.ceil(total / perPage));
|
||||||
|
if (pages <= 1) { bar.style.display = 'none'; return; }
|
||||||
|
_ensurePgnStyles();
|
||||||
|
const from = (page - 1) * perPage + 1;
|
||||||
|
const to = Math.min(page * perPage, total);
|
||||||
|
// page buttons with ellipsis: first, current±2, last
|
||||||
|
const nums = new Set([1, pages, page, page - 1, page + 1, page - 2, page + 2]);
|
||||||
|
const sorted = [...nums].filter(n => n >= 1 && n <= pages).sort((a, b) => a - b);
|
||||||
|
const numHtml = sorted.map((n, i) => {
|
||||||
|
const prev = sorted[i - 1];
|
||||||
|
const gap = prev && n - prev > 1 ? '<span class="pgn-ellip">…</span>' : '';
|
||||||
|
return `${gap}<button class="pgn-btn${n === page ? ' active' : ''}" onclick="${gotoFn}(${n})">${n}</button>`;
|
||||||
|
}).join('');
|
||||||
|
bar.innerHTML = `
|
||||||
|
<div class="pgn-info">${from}–${to} из ${total}</div>
|
||||||
|
<div class="pgn-ctrls">
|
||||||
|
<button class="pgn-btn" onclick="${gotoFn}(${page - 1})" ${page <= 1 ? 'disabled' : ''}>←</button>
|
||||||
|
${numHtml}
|
||||||
|
<button class="pgn-btn" onclick="${gotoFn}(${page + 1})" ${page >= pages ? 'disabled' : ''}>→</button>
|
||||||
|
</div>`;
|
||||||
|
bar.style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUsers(page) {
|
||||||
|
if (page) _usersPage = page;
|
||||||
try {
|
try {
|
||||||
const users = await LS.adminGetUsers();
|
const r = await LS.adminGetUsers({ page: _usersPage, limit: _USERS_PER_PAGE });
|
||||||
|
const users = r.users || [];
|
||||||
const tbody = document.getElementById('users-body');
|
const tbody = document.getElementById('users-body');
|
||||||
if (!users.length) { tbody.innerHTML = '<tr><td colspan="7"><div class="empty">Пользователей нет</div></td></tr>'; return; }
|
if (!users.length) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="7"><div class="empty">Пользователей нет</div></td></tr>';
|
||||||
|
document.getElementById('users-pagination').style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
tbody.innerHTML = users.map(u => {
|
tbody.innerHTML = users.map(u => {
|
||||||
const pc = pctClass(u.avg_pct);
|
const pc = pctClass(u.avg_pct);
|
||||||
const initials = (u.name||'?').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'?';
|
const initials = (u.name||'?').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'?';
|
||||||
@@ -691,12 +743,20 @@
|
|||||||
<td style="text-align:right;color:var(--text-3);font-size:0.85rem;opacity:0.4">›</td>
|
<td style="text-align:right;color:var(--text-3);font-size:0.85rem;opacity:0.4">›</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
_renderPgnControls('users-pagination', _usersPage, r.total || users.length, _USERS_PER_PAGE, 'gotoUsersPage');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.getElementById('users-body').innerHTML = `<tr><td colspan="7"></td></tr>`;
|
document.getElementById('users-body').innerHTML = `<tr><td colspan="7"></td></tr>`;
|
||||||
LS.state.error(document.getElementById('users-body').querySelector('td'), e, loadUsers);
|
LS.state.error(document.getElementById('users-body').querySelector('td'), e, loadUsers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function gotoUsersPage(n) {
|
||||||
|
_usersPage = n;
|
||||||
|
loadUsers();
|
||||||
|
document.getElementById('tab-users')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
window.gotoUsersPage = gotoUsersPage;
|
||||||
|
|
||||||
async function changeRole(select) {
|
async function changeRole(select) {
|
||||||
select.disabled = true;
|
select.disabled = true;
|
||||||
try { await LS.adminUpdateRole(select.dataset.uid, select.value); LS.toast('Роль изменена', 'success', 2000); }
|
try { await LS.adminUpdateRole(select.dataset.uid, select.value); LS.toast('Роль изменена', 'success', 2000); }
|
||||||
|
|||||||
@@ -156,9 +156,8 @@ async function adminGetUsers(params = {}) {
|
|||||||
if (params.limit) p.set('limit', params.limit);
|
if (params.limit) p.set('limit', params.limit);
|
||||||
if (params.role) p.set('role', params.role);
|
if (params.role) p.set('role', params.role);
|
||||||
if (params.q) p.set('q', params.q);
|
if (params.q) p.set('q', params.q);
|
||||||
const data = await req('GET', `/admin/users?${p}`);
|
// Returns { users, total, page, limit } (or { users, nextCursor, limit } if cursor used)
|
||||||
// API returns { users, total, page, limit } — extract users for compat
|
return req('GET', `/admin/users?${p}`);
|
||||||
return Array.isArray(data) ? data : data.users;
|
|
||||||
}
|
}
|
||||||
async function adminUpdateRole(id, role) { return req('PATCH', `/admin/users/${id}/role`, { role }); }
|
async function adminUpdateRole(id, role) { return req('PATCH', `/admin/users/${id}/role`, { role }); }
|
||||||
async function adminGetUserSessions(id) { return req('GET', `/admin/users/${id}/sessions`); }
|
async function adminGetUserSessions(id) { return req('GET', `/admin/users/${id}/sessions`); }
|
||||||
|
|||||||
Reference in New Issue
Block a user