feat(admin-configurator): add guest login screen, responsive navbar menu, fix player modals, sessions and audit logs (fixes #1, #2, #3, #5, #6, #7)
This commit is contained in:
1 parent
9f905569a5
commit
351d5f1cca
7 files changed
+1031
-235
No files matched your search
@@ -148,20 +148,17 @@ body {
|
||||
/* Nav Tabs */
|
||||
.tabs-navigation {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
overflow-x: visible;
|
||||
scrollbar-width: none;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
scrollbar-width: thin;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.tabs-navigation::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
.tabs-navigation::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 4px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-tab-btn {
|
||||
@@ -177,6 +174,7 @@ body {
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
@@ -654,7 +652,8 @@ body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.modal-overlay.open {
|
||||
.modal-overlay.open,
|
||||
.modal-overlay.active {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
@@ -821,10 +820,29 @@ input:checked + .slider-round:before {
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.controls-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.tabs-navigation {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 6px;
|
||||
overflow-x: visible;
|
||||
}
|
||||
.nav-tab-btn {
|
||||
justify-content: center;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.tabs-navigation {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1241,3 +1259,138 @@ input:checked + .slider-round:before {
|
||||
.player-name-link:hover {
|
||||
color: #a5d6a7;
|
||||
}
|
||||
|
||||
/* ==========================================================
|
||||
Full-Screen Login Screen for Guest State (Cyberpunk Botanical)
|
||||
========================================================== */
|
||||
.login-page-screen {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px 16px;
|
||||
background-color: var(--bg-main);
|
||||
background-image:
|
||||
radial-gradient(circle at 50% 30%, rgba(76, 175, 80, 0.12) 0%, transparent 60%),
|
||||
radial-gradient(circle at 80% 80%, rgba(0, 229, 255, 0.08) 0%, transparent 50%),
|
||||
linear-gradient(180deg, #090d13 0%, #0d121c 100%);
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
background: var(--bg-card);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-card), 0 0 40px rgba(76, 175, 80, 0.15);
|
||||
padding: 36px 32px;
|
||||
animation: loginCardFadeIn 0.35s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
@keyframes loginCardFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px) scale(0.98);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.login-card-header {
|
||||
text-align: center;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.login-brand-icon {
|
||||
font-size: 42px;
|
||||
margin-bottom: 8px;
|
||||
filter: drop-shadow(0 0 16px var(--accent-green-glow));
|
||||
animation: pulseGlow 3s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes pulseGlow {
|
||||
0% { transform: scale(1); filter: drop-shadow(0 0 12px var(--accent-green-glow)); }
|
||||
100% { transform: scale(1.05); filter: drop-shadow(0 0 24px var(--accent-cyan-glow)); }
|
||||
}
|
||||
|
||||
.login-brand-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
letter-spacing: -0.3px;
|
||||
margin-bottom: 8px;
|
||||
text-shadow: 0 0 20px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.login-brand-subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.password-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.password-input-wrapper .form-input {
|
||||
padding-right: 44px;
|
||||
}
|
||||
|
||||
.btn-toggle-pwd {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-dim);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.btn-toggle-pwd:hover {
|
||||
color: var(--text-main);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.login-error-alert {
|
||||
background: rgba(255, 82, 82, 0.15);
|
||||
border: 1px solid rgba(255, 82, 82, 0.45);
|
||||
color: #ff8a80;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
animation: shakeAlert 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes shakeAlert {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-4px); }
|
||||
75% { transform: translateX(4px); }
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
@@ -8,7 +8,31 @@
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- Dedicated Full-Screen Login Screen for Guests -->
|
||||
<div id="configurator-login-screen" class="login-page-screen">
|
||||
<div class="login-card">
|
||||
<div class="login-card-header">
|
||||
<div class="login-brand-icon">🌻</div>
|
||||
<h1 class="login-brand-title">🌻 ⚙️ Панель Администратора PvZ</h1>
|
||||
<p class="login-brand-subtitle">Вход в систему управления и конфигуратор баланса игры</p>
|
||||
</div>
|
||||
<form id="configurator-login-form" class="login-form" onsubmit="return false;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="login-screen-password">Пароль администратора:</label>
|
||||
<div class="password-input-wrapper">
|
||||
<input type="password" id="login-screen-password" class="form-input" placeholder="Введите мастер-пароль..." autocomplete="current-password" required>
|
||||
<button type="button" id="btn-toggle-login-password" class="btn-toggle-pwd" title="Показать/скрыть пароль" tabindex="-1">👁️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="login-screen-error" class="login-error-alert" style="display: none;"></div>
|
||||
<button type="submit" id="btn-login-screen-submit" class="btn btn-primary btn-block">Войти в систему 🔐</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Configurator Application -->
|
||||
<div id="configurator-app-container" style="display: none;">
|
||||
<div class="app-container">
|
||||
<!-- Header -->
|
||||
<header class="app-header">
|
||||
<div class="brand">
|
||||
@@ -900,6 +924,7 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL: LOGIN -->
|
||||
<div id="modal-login" class="modal-overlay">
|
||||
|
||||
@@ -33,11 +33,9 @@ class PvZConfiguratorApp {
|
||||
this.onAuthenticated();
|
||||
} else {
|
||||
this.onUnauthenticated();
|
||||
this.loadPublicConfig();
|
||||
}
|
||||
} else {
|
||||
this.onUnauthenticated();
|
||||
this.loadPublicConfig();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,24 +101,116 @@ class PvZConfiguratorApp {
|
||||
|
||||
onAuthenticated() {
|
||||
this.isAuthenticated = true;
|
||||
document.getElementById('admin-status-dot').className = 'status-dot';
|
||||
document.getElementById('admin-status-text').textContent = 'Администратор';
|
||||
document.getElementById('btn-login-modal-open').style.display = 'none';
|
||||
|
||||
const loginScreen = document.getElementById('configurator-login-screen');
|
||||
if (loginScreen) loginScreen.style.display = 'none';
|
||||
|
||||
const appContainer = document.getElementById('configurator-app-container');
|
||||
if (appContainer) appContainer.style.display = 'block';
|
||||
|
||||
const dot = document.getElementById('admin-status-dot');
|
||||
if (dot) dot.className = 'status-dot';
|
||||
const stText = document.getElementById('admin-status-text');
|
||||
if (stText) stText.textContent = 'Администратор';
|
||||
|
||||
const btnLoginModal = document.getElementById('btn-login-modal-open');
|
||||
if (btnLoginModal) btnLoginModal.style.display = 'none';
|
||||
|
||||
const btnPwd = document.getElementById('btn-admin-password-modal-open');
|
||||
if (btnPwd) btnPwd.style.display = 'inline-flex';
|
||||
document.getElementById('btn-admin-logout').style.display = 'inline-flex';
|
||||
|
||||
const btnLogout = document.getElementById('btn-admin-logout');
|
||||
if (btnLogout) btnLogout.style.display = 'inline-flex';
|
||||
|
||||
this.loadAllData();
|
||||
}
|
||||
|
||||
onUnauthenticated() {
|
||||
this.isAuthenticated = false;
|
||||
document.getElementById('admin-status-dot').className = 'status-dot offline';
|
||||
document.getElementById('admin-status-text').textContent = 'Гость (Только чтение)';
|
||||
document.getElementById('btn-login-modal-open').style.display = 'inline-flex';
|
||||
this.token = '';
|
||||
localStorage.removeItem('pvz_admin_token');
|
||||
this.config = null;
|
||||
this.stats = null;
|
||||
this.users = [];
|
||||
this.sessions = [];
|
||||
this.loginLogs = [];
|
||||
this.bugs = [];
|
||||
this.levels = [];
|
||||
|
||||
const appContainer = document.getElementById('configurator-app-container');
|
||||
if (appContainer) appContainer.style.display = 'none';
|
||||
|
||||
const loginScreen = document.getElementById('configurator-login-screen');
|
||||
if (loginScreen) loginScreen.style.display = 'flex';
|
||||
|
||||
const loginErr = document.getElementById('login-screen-error');
|
||||
if (loginErr) {
|
||||
loginErr.style.display = 'none';
|
||||
loginErr.textContent = '';
|
||||
}
|
||||
|
||||
const pwdInput = document.getElementById('login-screen-password');
|
||||
if (pwdInput) pwdInput.value = '';
|
||||
|
||||
const dot = document.getElementById('admin-status-dot');
|
||||
if (dot) dot.className = 'status-dot offline';
|
||||
const stText = document.getElementById('admin-status-text');
|
||||
if (stText) stText.textContent = 'Гость (Только чтение)';
|
||||
|
||||
const btnLoginModal = document.getElementById('btn-login-modal-open');
|
||||
if (btnLoginModal) btnLoginModal.style.display = 'inline-flex';
|
||||
|
||||
const btnPwd = document.getElementById('btn-admin-password-modal-open');
|
||||
if (btnPwd) btnPwd.style.display = 'none';
|
||||
document.getElementById('btn-admin-logout').style.display = 'none';
|
||||
|
||||
const btnLogout = document.getElementById('btn-admin-logout');
|
||||
if (btnLogout) btnLogout.style.display = 'none';
|
||||
}
|
||||
|
||||
openModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.add('active', 'open');
|
||||
}
|
||||
}
|
||||
|
||||
closeModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.remove('active', 'open');
|
||||
}
|
||||
}
|
||||
|
||||
async handleScreenLogin() {
|
||||
const pwdInput = document.getElementById('login-screen-password');
|
||||
const errDiv = document.getElementById('login-screen-error');
|
||||
const enteredPassword = pwdInput ? pwdInput.value.trim() : '';
|
||||
|
||||
if (!enteredPassword) {
|
||||
if (errDiv) {
|
||||
errDiv.textContent = 'Введите мастер-пароль администратора';
|
||||
errDiv.style.display = 'flex';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (errDiv) {
|
||||
errDiv.style.display = 'none';
|
||||
errDiv.textContent = '';
|
||||
}
|
||||
|
||||
const res = await this.apiRequest('/api/admin/auth', 'POST', { password: enteredPassword });
|
||||
if (res.ok && res.data && res.data.success && res.data.token) {
|
||||
this.token = res.data.token;
|
||||
localStorage.setItem('pvz_admin_token', this.token);
|
||||
this.showToast('Авторизация успешна! Добро пожаловать в панель управления.', 'success');
|
||||
this.onAuthenticated();
|
||||
} else {
|
||||
if (errDiv) {
|
||||
errDiv.textContent = (res.data && res.data.message) || 'Неверный пароль администратора';
|
||||
errDiv.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadPublicConfig() {
|
||||
@@ -746,20 +836,24 @@ class PvZConfiguratorApp {
|
||||
filtered.forEach(s => {
|
||||
const tr = document.createElement('tr');
|
||||
const tokenShort = (s.token || '').substring(0, 10) + '...';
|
||||
const isOnline = true;
|
||||
const username = s.username || 'Гость';
|
||||
const ipAddress = s.ipAddress || '127.0.0.1';
|
||||
const deviceName = (s.deviceName && s.deviceName !== 'Unknown Device') ? s.deviceName : 'Браузер';
|
||||
const createdAt = s.createdAt || '—';
|
||||
const lastActive = s.lastActive || s.createdAt || 'Только что';
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<span class="player-name-link" data-user-id="${s.userId}">${s.username || 'Гость'}</span>
|
||||
<span class="player-name-link" data-user-id="${s.userId || ''}">${username}</span>
|
||||
${s.isAdmin ? '<span class="badge badge-warning" style="font-size:10px; margin-left:4px;">ADMIN</span>' : ''}
|
||||
</td>
|
||||
<td><span style="font-family:monospace; color:var(--text-muted);">${s.ipAddress || '127.0.0.1'}</span></td>
|
||||
<td><span style="color:var(--text-main); font-size:12px;">${s.deviceName || 'Браузер'}</span></td>
|
||||
<td style="color:var(--text-dim); font-size:12px;">${s.createdAt || '—'}</td>
|
||||
<td><span style="color:#a5d6a7; font-size:12px;">🟢 ${s.lastActive || s.createdAt || 'Только что'}</span></td>
|
||||
<td><span class="token-cell" title="${s.token}">${tokenShort}</span></td>
|
||||
<td><span style="font-family:monospace; color:var(--text-muted); font-size:12px;">${ipAddress}</span></td>
|
||||
<td><span style="color:var(--text-main); font-size:12px;">${deviceName}</span></td>
|
||||
<td style="color:var(--text-dim); font-size:12px;">${createdAt}</td>
|
||||
<td><span style="color:#a5d6a7; font-size:12px;">🟢 ${lastActive}</span></td>
|
||||
<td><span class="token-cell" title="${s.token || ''}">${tokenShort}</span></td>
|
||||
<td>
|
||||
<button class="btn btn-danger btn-sm btn-terminate-session" data-token="${s.token}" title="Завершить сессию игрока">
|
||||
<button class="btn btn-danger btn-sm btn-terminate-session" data-token="${s.token || ''}" title="Завершить сессию игрока">
|
||||
🚪 Завершить
|
||||
</button>
|
||||
</td>
|
||||
@@ -825,7 +919,10 @@ class PvZConfiguratorApp {
|
||||
filtered = filtered.filter(l => l.username && l.username.toLowerCase().includes(userFilter));
|
||||
}
|
||||
if (ipFilter) {
|
||||
filtered = filtered.filter(l => l.ipAddress && l.ipAddress.toLowerCase().includes(ipFilter));
|
||||
filtered = filtered.filter(l =>
|
||||
(l.ipAddress && l.ipAddress.toLowerCase().includes(ipFilter)) ||
|
||||
(l.ip && l.ip.toLowerCase().includes(ipFilter))
|
||||
);
|
||||
}
|
||||
if (statusFilter !== 'all') {
|
||||
filtered = filtered.filter(l => l.status === statusFilter);
|
||||
@@ -839,27 +936,32 @@ class PvZConfiguratorApp {
|
||||
tbody.innerHTML = '';
|
||||
filtered.forEach(log => {
|
||||
const tr = document.createElement('tr');
|
||||
const timestamp = log.createdAt || log.timestamp || '—';
|
||||
const username = log.username || 'Гость';
|
||||
const ipAddress = log.ipAddress || log.ip || '127.0.0.1';
|
||||
const deviceName = (log.deviceName && log.deviceName !== 'Unknown Device') ? log.deviceName : 'Браузер';
|
||||
const status = log.status || 'unknown';
|
||||
|
||||
let statusBadge = `<span class="badge badge-muted">${log.status}</span>`;
|
||||
if (log.status === 'success') {
|
||||
let statusBadge = `<span class="badge badge-muted">${status}</span>`;
|
||||
if (status === 'success') {
|
||||
statusBadge = `<span class="badge badge-success">✅ Успешно</span>`;
|
||||
} else if (log.status === 'registered') {
|
||||
} else if (status === 'registered') {
|
||||
statusBadge = `<span class="badge badge-purple">🎉 Регистрация</span>`;
|
||||
} else if (log.status === 'wrong_password') {
|
||||
} else if (status === 'wrong_password') {
|
||||
statusBadge = `<span class="badge badge-danger">❌ Неверный пароль</span>`;
|
||||
} else if (log.status === 'banned_login_attempt') {
|
||||
} else if (status === 'banned_login_attempt') {
|
||||
statusBadge = `<span class="badge badge-warning">🚫 Заблокирован</span>`;
|
||||
} else if (log.status === 'rate_limited') {
|
||||
} else if (status === 'rate_limited') {
|
||||
statusBadge = `<span class="badge badge-warning">⚠️ Превышен лимит</span>`;
|
||||
} else if (log.status === 'user_not_found') {
|
||||
} else if (status === 'user_not_found') {
|
||||
statusBadge = `<span class="badge badge-muted">❓ Не найден</span>`;
|
||||
}
|
||||
|
||||
tr.innerHTML = `
|
||||
<td style="color:var(--text-dim); font-size:12px; font-family:monospace;">${log.timestamp || '—'}</td>
|
||||
<td><b style="color:var(--accent-cyan);">${log.username || '—'}</b></td>
|
||||
<td><span style="font-family:monospace; color:var(--text-muted); font-size:12px;">${log.ipAddress || '—'}</span></td>
|
||||
<td style="font-size:12px; color:var(--text-main);">${log.deviceName || 'Браузер'}</td>
|
||||
<td style="color:var(--text-dim); font-size:12px; font-family:monospace;">${timestamp}</td>
|
||||
<td><b style="color:var(--accent-cyan);">${username}</b></td>
|
||||
<td><span style="font-family:monospace; color:var(--text-muted); font-size:12px;">${ipAddress}</span></td>
|
||||
<td style="font-size:12px; color:var(--text-main);">${deviceName}</td>
|
||||
<td>${statusBadge}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
@@ -902,7 +1004,7 @@ class PvZConfiguratorApp {
|
||||
|
||||
if (res.ok && res.data.success) {
|
||||
this.showToast('Мастер-пароль администратора успешно изменен!', 'success');
|
||||
document.getElementById('modal-admin-password').classList.remove('active');
|
||||
this.closeModal('modal-admin-password');
|
||||
document.getElementById('admin-current-password').value = '';
|
||||
document.getElementById('admin-new-password').value = '';
|
||||
document.getElementById('admin-confirm-password').value = '';
|
||||
@@ -987,8 +1089,13 @@ class PvZConfiguratorApp {
|
||||
}
|
||||
|
||||
async openPlayerStatsModal(userId) {
|
||||
if (!this.isAuthenticated) {
|
||||
this.showToast('Требуется авторизация администратора', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await this.apiRequest(`/api/admin/user/${userId}`);
|
||||
if (!res.ok || !res.data.success) {
|
||||
if (!res.ok || !res.data || !res.data.success) {
|
||||
this.showToast('Ошибка загрузки статистики игрока', 'error');
|
||||
return;
|
||||
}
|
||||
@@ -1003,7 +1110,7 @@ class PvZConfiguratorApp {
|
||||
const idInput = document.getElementById('stats-player-id');
|
||||
if (idInput) idInput.value = u.id;
|
||||
|
||||
document.getElementById('stats-player-username').textContent = u.username;
|
||||
document.getElementById('stats-player-username').textContent = u.username || '—';
|
||||
document.getElementById('stats-player-id-line').textContent = `#${u.id} (${prof.theme || 'classic'})`;
|
||||
document.getElementById('stats-player-created').textContent = u.createdAt || '—';
|
||||
document.getElementById('stats-player-login').textContent = u.lastLogin || '—';
|
||||
@@ -1041,12 +1148,17 @@ class PvZConfiguratorApp {
|
||||
}
|
||||
document.getElementById('stats-scores-summary').innerHTML = scoreTxt;
|
||||
|
||||
document.getElementById('modal-player-stats').classList.add('active');
|
||||
this.openModal('modal-player-stats');
|
||||
}
|
||||
|
||||
async openEditPlayerModal(userId) {
|
||||
if (!this.isAuthenticated) {
|
||||
this.showToast('Требуется авторизация администратора', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await this.apiRequest(`/api/admin/user/${userId}`);
|
||||
if (!res.ok || !res.data.success) {
|
||||
if (!res.ok || !res.data || !res.data.success) {
|
||||
this.showToast('Ошибка загрузки профиля игрока', 'error');
|
||||
return;
|
||||
}
|
||||
@@ -1057,7 +1169,7 @@ class PvZConfiguratorApp {
|
||||
const zen = prof.zenGarden || u.zenGarden || {};
|
||||
|
||||
document.getElementById('edit-player-id').value = u.id;
|
||||
document.getElementById('edit-player-title-name').textContent = u.username;
|
||||
document.getElementById('edit-player-title-name').textContent = u.username || '';
|
||||
document.getElementById('edit-player-coins').value = prof.coins ?? u.coins ?? 150;
|
||||
document.getElementById('edit-player-level').value = prof.level ?? u.level ?? 1;
|
||||
document.getElementById('edit-player-maxlevel').value = prof.maxUnlockedLevel ?? u.maxUnlockedLevel ?? 1;
|
||||
@@ -1081,10 +1193,15 @@ class PvZConfiguratorApp {
|
||||
const banCont = document.getElementById('edit-ban-reason-container');
|
||||
if (banCont) banCont.style.display = u.isBanned ? 'block' : 'none';
|
||||
|
||||
document.getElementById('modal-edit-player').classList.add('active');
|
||||
this.openModal('modal-edit-player');
|
||||
}
|
||||
|
||||
async savePlayerEdit() {
|
||||
if (!this.isAuthenticated) {
|
||||
this.showToast('Требуется авторизация администратора', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const id = document.getElementById('edit-player-id').value;
|
||||
const updates = {
|
||||
coins: parseInt(document.getElementById('edit-player-coins').value, 10) || 0,
|
||||
@@ -1113,17 +1230,22 @@ class PvZConfiguratorApp {
|
||||
};
|
||||
|
||||
const res = await this.apiRequest(`/api/admin/user/${id}/edit`, 'POST', { updates });
|
||||
if (res.ok && res.data.success) {
|
||||
if (res.ok && res.data && res.data.success) {
|
||||
this.showToast('Профиль игрока и ресурсы успешно обновлены!', 'success');
|
||||
document.getElementById('modal-edit-player').classList.remove('active');
|
||||
this.closeModal('modal-edit-player');
|
||||
this.loadUsers();
|
||||
this.loadStats();
|
||||
} else {
|
||||
this.showToast(res.data.message || 'Ошибка сохранения', 'error');
|
||||
this.showToast((res.data && res.data.message) || 'Ошибка сохранения', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async createPlayer() {
|
||||
if (!this.isAuthenticated) {
|
||||
this.showToast('Требуется авторизация администратора', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const username = document.getElementById('create-player-username').value.trim();
|
||||
const password = document.getElementById('create-player-password').value.trim();
|
||||
const coins = parseInt(document.getElementById('create-player-coins').value, 10) || 150;
|
||||
@@ -1141,26 +1263,36 @@ class PvZConfiguratorApp {
|
||||
initialData: { coins, level: 1, maxUnlockedLevel: 1 }
|
||||
});
|
||||
|
||||
if (res.ok && res.data.success) {
|
||||
this.showToast(`Игрок ${username} создан!`, 'success');
|
||||
document.getElementById('modal-create-player').classList.remove('active');
|
||||
if (res.ok && res.data && res.data.success) {
|
||||
this.showToast(`Игрок ${username} успешно создан!`, 'success');
|
||||
this.closeModal('modal-create-player');
|
||||
document.getElementById('create-player-username').value = '';
|
||||
document.getElementById('create-player-password').value = '';
|
||||
this.loadUsers();
|
||||
this.loadStats();
|
||||
} else {
|
||||
this.showToast(res.data.message || 'Ошибка создания игрока', 'error');
|
||||
this.showToast((res.data && res.data.message) || 'Ошибка создания игрока', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
openResetPasswordModal(id, username) {
|
||||
if (!this.isAuthenticated) {
|
||||
this.showToast('Требуется авторизация администратора', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('reset-pwd-user-id').value = id;
|
||||
document.getElementById('reset-pwd-username').textContent = username;
|
||||
document.getElementById('reset-pwd-input').value = '';
|
||||
document.getElementById('modal-reset-password').classList.add('active');
|
||||
this.openModal('modal-reset-password');
|
||||
}
|
||||
|
||||
async submitResetPassword() {
|
||||
if (!this.isAuthenticated) {
|
||||
this.showToast('Требуется авторизация администратора', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const id = document.getElementById('reset-pwd-user-id').value;
|
||||
const pwd = document.getElementById('reset-pwd-input').value.trim();
|
||||
if (!pwd) {
|
||||
@@ -1169,11 +1301,11 @@ class PvZConfiguratorApp {
|
||||
}
|
||||
|
||||
const res = await this.apiRequest(`/api/admin/user/${id}/password`, 'POST', { password: pwd });
|
||||
if (res.ok && res.data.success) {
|
||||
if (res.ok && res.data && res.data.success) {
|
||||
this.showToast('Пароль успешно сброшен', 'success');
|
||||
document.getElementById('modal-reset-password').classList.remove('active');
|
||||
this.closeModal('modal-reset-password');
|
||||
} else {
|
||||
this.showToast(res.data.message || 'Ошибка сброса пароля', 'error');
|
||||
this.showToast((res.data && res.data.message) || 'Ошибка сброса пароля', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1254,7 +1386,7 @@ class PvZConfiguratorApp {
|
||||
document.getElementById('bug-mod-status').value = b.status || 'новый';
|
||||
document.getElementById('bug-mod-reply').value = b.adminReply || '';
|
||||
|
||||
document.getElementById('modal-moderate-bug').classList.add('active');
|
||||
this.openModal('modal-moderate-bug');
|
||||
}
|
||||
|
||||
async saveBugModeration() {
|
||||
@@ -1265,7 +1397,7 @@ class PvZConfiguratorApp {
|
||||
const res = await this.apiRequest(`/api/admin/bugs/${id}/status`, 'POST', { status, adminReply });
|
||||
if (res.ok && res.data.success) {
|
||||
this.showToast(`Баг #${id} обновлен`, 'success');
|
||||
document.getElementById('modal-moderate-bug').classList.remove('active');
|
||||
this.closeModal('modal-moderate-bug');
|
||||
this.loadBugs();
|
||||
} else {
|
||||
this.showToast(res.data.message || 'Ошибка сохранения', 'error');
|
||||
@@ -1279,7 +1411,7 @@ class PvZConfiguratorApp {
|
||||
const res = await this.apiRequest(`/api/admin/bugs/${id}/status`, 'POST', { delete: true });
|
||||
if (res.ok && res.data.success) {
|
||||
this.showToast(`Баг #${id} удален`, 'success');
|
||||
document.getElementById('modal-moderate-bug').classList.remove('active');
|
||||
this.closeModal('modal-moderate-bug');
|
||||
this.loadBugs();
|
||||
} else {
|
||||
this.showToast(res.data.message || 'Ошибка удаления', 'error');
|
||||
@@ -1434,46 +1566,94 @@ class PvZConfiguratorApp {
|
||||
if (pane) pane.classList.add('active');
|
||||
this.activeTab = target;
|
||||
|
||||
if (target === 'tab-levels' && this.levels.length === 0) this.loadCampaignLevels();
|
||||
if (target === 'tab-players') this.loadUsers();
|
||||
if (target === 'tab-sessions') this.loadSessions();
|
||||
if (target === 'tab-logs') this.loadLoginLogs();
|
||||
if (target === 'tab-levels' && this.levels.length === 0) this.loadCampaignLevels();
|
||||
});
|
||||
});
|
||||
|
||||
// Full-Screen Login Screen Bindings
|
||||
const btnScreenSubmit = document.getElementById('btn-login-screen-submit');
|
||||
if (btnScreenSubmit) {
|
||||
btnScreenSubmit.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.handleScreenLogin();
|
||||
});
|
||||
}
|
||||
|
||||
const screenPwdInput = document.getElementById('login-screen-password');
|
||||
if (screenPwdInput) {
|
||||
screenPwdInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.handleScreenLogin();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const btnTogglePwd = document.getElementById('btn-toggle-login-password');
|
||||
if (btnTogglePwd && screenPwdInput) {
|
||||
btnTogglePwd.addEventListener('click', () => {
|
||||
if (screenPwdInput.type === 'password') {
|
||||
screenPwdInput.type = 'text';
|
||||
btnTogglePwd.textContent = '🙈';
|
||||
} else {
|
||||
screenPwdInput.type = 'password';
|
||||
btnTogglePwd.textContent = '👁️';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Login modal
|
||||
document.getElementById('btn-login-modal-open').addEventListener('click', () => {
|
||||
document.getElementById('login-error-msg').style.display = 'none';
|
||||
document.getElementById('admin-password-input').value = '';
|
||||
document.getElementById('modal-login').classList.add('active');
|
||||
});
|
||||
|
||||
document.getElementById('btn-submit-login').addEventListener('click', async () => {
|
||||
const pwd = document.getElementById('admin-password-input').value.trim();
|
||||
if (!pwd) return;
|
||||
|
||||
const res = await this.apiRequest('/api/admin/auth', 'POST', { password: pwd });
|
||||
if (res.ok && res.data.success) {
|
||||
this.token = res.data.token;
|
||||
localStorage.setItem('pvz_admin_token', this.token);
|
||||
this.onAuthenticated();
|
||||
document.getElementById('modal-login').classList.remove('active');
|
||||
this.showToast('Авторизация администратора успешна!', 'success');
|
||||
} else {
|
||||
const btnLoginOpen = document.getElementById('btn-login-modal-open');
|
||||
if (btnLoginOpen) {
|
||||
btnLoginOpen.addEventListener('click', () => {
|
||||
const err = document.getElementById('login-error-msg');
|
||||
err.textContent = res.data.message || 'Неверный пароль администратора';
|
||||
err.style.display = 'block';
|
||||
}
|
||||
});
|
||||
if (err) err.style.display = 'none';
|
||||
const input = document.getElementById('admin-password-input');
|
||||
if (input) input.value = '';
|
||||
this.openModal('modal-login');
|
||||
});
|
||||
}
|
||||
|
||||
const btnSubmitLogin = document.getElementById('btn-submit-login');
|
||||
if (btnSubmitLogin) {
|
||||
btnSubmitLogin.addEventListener('click', async () => {
|
||||
const input = document.getElementById('admin-password-input');
|
||||
const pwd = input ? input.value.trim() : '';
|
||||
if (!pwd) return;
|
||||
|
||||
const res = await this.apiRequest('/api/admin/auth', 'POST', { password: pwd });
|
||||
if (res.ok && res.data && res.data.success && res.data.token) {
|
||||
this.token = res.data.token;
|
||||
localStorage.setItem('pvz_admin_token', this.token);
|
||||
this.closeModal('modal-login');
|
||||
this.showToast('Авторизация администратора успешна!', 'success');
|
||||
this.onAuthenticated();
|
||||
} else {
|
||||
const err = document.getElementById('login-error-msg');
|
||||
if (err) {
|
||||
err.textContent = (res.data && res.data.message) || 'Неверный пароль администратора';
|
||||
err.style.display = 'block';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Change Admin Password Modal
|
||||
const btnOpenPwd = document.getElementById('btn-admin-password-modal-open');
|
||||
if (btnOpenPwd) {
|
||||
btnOpenPwd.addEventListener('click', () => {
|
||||
document.getElementById('admin-password-error').style.display = 'none';
|
||||
document.getElementById('admin-current-password').value = '';
|
||||
document.getElementById('admin-new-password').value = '';
|
||||
document.getElementById('admin-confirm-password').value = '';
|
||||
document.getElementById('modal-admin-password').classList.add('active');
|
||||
const err = document.getElementById('admin-password-error');
|
||||
if (err) err.style.display = 'none';
|
||||
const cur = document.getElementById('admin-current-password');
|
||||
if (cur) cur.value = '';
|
||||
const nw = document.getElementById('admin-new-password');
|
||||
if (nw) nw.value = '';
|
||||
const cf = document.getElementById('admin-confirm-password');
|
||||
if (cf) cf.value = '';
|
||||
this.openModal('modal-admin-password');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1483,19 +1663,29 @@ class PvZConfiguratorApp {
|
||||
}
|
||||
|
||||
// Logout
|
||||
document.getElementById('btn-admin-logout').addEventListener('click', () => {
|
||||
this.token = '';
|
||||
localStorage.removeItem('pvz_admin_token');
|
||||
this.onUnauthenticated();
|
||||
this.showToast('Вы вышли из панели администратора', 'info');
|
||||
});
|
||||
const btnLogout = document.getElementById('btn-admin-logout');
|
||||
if (btnLogout) {
|
||||
btnLogout.addEventListener('click', () => {
|
||||
this.token = '';
|
||||
localStorage.removeItem('pvz_admin_token');
|
||||
this.onUnauthenticated();
|
||||
this.showToast('Вы вышли из панели администратора', 'info');
|
||||
});
|
||||
}
|
||||
|
||||
// Close Modals
|
||||
document.querySelectorAll('[data-close]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const modalId = btn.getAttribute('data-close');
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) modal.classList.remove('active');
|
||||
this.closeModal(modalId);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) {
|
||||
overlay.classList.remove('active', 'open');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1539,26 +1729,56 @@ class PvZConfiguratorApp {
|
||||
});
|
||||
|
||||
// Player Actions
|
||||
document.getElementById('btn-create-player-open').addEventListener('click', () => {
|
||||
document.getElementById('modal-create-player').classList.add('active');
|
||||
});
|
||||
document.getElementById('btn-submit-create-player').addEventListener('click', () => this.createPlayer());
|
||||
document.getElementById('btn-save-player-edit').addEventListener('click', () => this.savePlayerEdit());
|
||||
document.getElementById('btn-submit-reset-pwd').addEventListener('click', () => this.submitResetPassword());
|
||||
document.getElementById('btn-refresh-players').addEventListener('click', () => this.loadUsers());
|
||||
document.getElementById('player-search-input').addEventListener('input', () => this.renderPlayersTable());
|
||||
document.getElementById('player-filter-status').addEventListener('change', () => this.renderPlayersTable());
|
||||
document.getElementById('player-sort-select').addEventListener('change', () => this.renderPlayersTable());
|
||||
document.getElementById('edit-player-is-banned').addEventListener('change', (e) => {
|
||||
document.getElementById('edit-ban-reason-container').style.display = e.target.checked ? 'block' : 'none';
|
||||
});
|
||||
const btnCreatePlayerOpen = document.getElementById('btn-create-player-open');
|
||||
if (btnCreatePlayerOpen) {
|
||||
btnCreatePlayerOpen.addEventListener('click', () => {
|
||||
this.openModal('modal-create-player');
|
||||
});
|
||||
}
|
||||
const btnSubmitCreatePlayer = document.getElementById('btn-submit-create-player');
|
||||
if (btnSubmitCreatePlayer) {
|
||||
btnSubmitCreatePlayer.addEventListener('click', () => this.createPlayer());
|
||||
}
|
||||
const btnSavePlayerEdit = document.getElementById('btn-save-player-edit');
|
||||
if (btnSavePlayerEdit) {
|
||||
btnSavePlayerEdit.addEventListener('click', () => this.savePlayerEdit());
|
||||
}
|
||||
const btnSubmitResetPwd = document.getElementById('btn-submit-reset-pwd');
|
||||
if (btnSubmitResetPwd) {
|
||||
btnSubmitResetPwd.addEventListener('click', () => this.submitResetPassword());
|
||||
}
|
||||
const btnRefreshPlayers = document.getElementById('btn-refresh-players');
|
||||
if (btnRefreshPlayers) {
|
||||
btnRefreshPlayers.addEventListener('click', () => this.loadUsers());
|
||||
}
|
||||
const playerSearchInput = document.getElementById('player-search-input');
|
||||
if (playerSearchInput) {
|
||||
playerSearchInput.addEventListener('input', () => this.renderPlayersTable());
|
||||
}
|
||||
const playerFilterStatus = document.getElementById('player-filter-status');
|
||||
if (playerFilterStatus) {
|
||||
playerFilterStatus.addEventListener('change', () => this.renderPlayersTable());
|
||||
}
|
||||
const playerSortSelect = document.getElementById('player-sort-select');
|
||||
if (playerSortSelect) {
|
||||
playerSortSelect.addEventListener('change', () => this.renderPlayersTable());
|
||||
}
|
||||
const editPlayerIsBanned = document.getElementById('edit-player-is-banned');
|
||||
if (editPlayerIsBanned) {
|
||||
editPlayerIsBanned.addEventListener('change', (e) => {
|
||||
const container = document.getElementById('edit-ban-reason-container');
|
||||
if (container) container.style.display = e.target.checked ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('btn-stats-jump-edit')?.addEventListener('click', () => {
|
||||
const id = document.getElementById('stats-player-id')?.value;
|
||||
const modal = document.getElementById('modal-player-stats');
|
||||
if (modal) modal.classList.remove('active');
|
||||
if (id) this.openEditPlayerModal(id);
|
||||
});
|
||||
const btnStatsJumpEdit = document.getElementById('btn-stats-jump-edit');
|
||||
if (btnStatsJumpEdit) {
|
||||
btnStatsJumpEdit.addEventListener('click', () => {
|
||||
const id = document.getElementById('stats-player-id')?.value;
|
||||
this.closeModal('modal-player-stats');
|
||||
if (id) this.openEditPlayerModal(id);
|
||||
});
|
||||
}
|
||||
|
||||
// Balance Save Buttons
|
||||
document.getElementById('btn-save-economy').addEventListener('click', () => this.saveBalanceSection('economy'));
|
||||
|
||||
@@ -148,20 +148,17 @@ body {
|
||||
/* Nav Tabs */
|
||||
.tabs-navigation {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
overflow-x: visible;
|
||||
scrollbar-width: none;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
scrollbar-width: thin;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.tabs-navigation::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
.tabs-navigation::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 4px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-tab-btn {
|
||||
@@ -177,6 +174,7 @@ body {
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
@@ -654,7 +652,8 @@ body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.modal-overlay.open {
|
||||
.modal-overlay.open,
|
||||
.modal-overlay.active {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
@@ -821,10 +820,29 @@ input:checked + .slider-round:before {
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.controls-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.tabs-navigation {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 6px;
|
||||
overflow-x: visible;
|
||||
}
|
||||
.nav-tab-btn {
|
||||
justify-content: center;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.tabs-navigation {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1241,3 +1259,138 @@ input:checked + .slider-round:before {
|
||||
.player-name-link:hover {
|
||||
color: #a5d6a7;
|
||||
}
|
||||
|
||||
/* ==========================================================
|
||||
Full-Screen Login Screen for Guest State (Cyberpunk Botanical)
|
||||
========================================================== */
|
||||
.login-page-screen {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px 16px;
|
||||
background-color: var(--bg-main);
|
||||
background-image:
|
||||
radial-gradient(circle at 50% 30%, rgba(76, 175, 80, 0.12) 0%, transparent 60%),
|
||||
radial-gradient(circle at 80% 80%, rgba(0, 229, 255, 0.08) 0%, transparent 50%),
|
||||
linear-gradient(180deg, #090d13 0%, #0d121c 100%);
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
background: var(--bg-card);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-card), 0 0 40px rgba(76, 175, 80, 0.15);
|
||||
padding: 36px 32px;
|
||||
animation: loginCardFadeIn 0.35s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
@keyframes loginCardFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px) scale(0.98);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.login-card-header {
|
||||
text-align: center;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.login-brand-icon {
|
||||
font-size: 42px;
|
||||
margin-bottom: 8px;
|
||||
filter: drop-shadow(0 0 16px var(--accent-green-glow));
|
||||
animation: pulseGlow 3s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes pulseGlow {
|
||||
0% { transform: scale(1); filter: drop-shadow(0 0 12px var(--accent-green-glow)); }
|
||||
100% { transform: scale(1.05); filter: drop-shadow(0 0 24px var(--accent-cyan-glow)); }
|
||||
}
|
||||
|
||||
.login-brand-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
letter-spacing: -0.3px;
|
||||
margin-bottom: 8px;
|
||||
text-shadow: 0 0 20px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.login-brand-subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.password-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.password-input-wrapper .form-input {
|
||||
padding-right: 44px;
|
||||
}
|
||||
|
||||
.btn-toggle-pwd {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-dim);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.btn-toggle-pwd:hover {
|
||||
color: var(--text-main);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.login-error-alert {
|
||||
background: rgba(255, 82, 82, 0.15);
|
||||
border: 1px solid rgba(255, 82, 82, 0.45);
|
||||
color: #ff8a80;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
animation: shakeAlert 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes shakeAlert {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-4px); }
|
||||
75% { transform: translateX(4px); }
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
@@ -8,7 +8,31 @@
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- Dedicated Full-Screen Login Screen for Guests -->
|
||||
<div id="configurator-login-screen" class="login-page-screen">
|
||||
<div class="login-card">
|
||||
<div class="login-card-header">
|
||||
<div class="login-brand-icon">🌻</div>
|
||||
<h1 class="login-brand-title">🌻 ⚙️ Панель Администратора PvZ</h1>
|
||||
<p class="login-brand-subtitle">Вход в систему управления и конфигуратор баланса игры</p>
|
||||
</div>
|
||||
<form id="configurator-login-form" class="login-form" onsubmit="return false;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="login-screen-password">Пароль администратора:</label>
|
||||
<div class="password-input-wrapper">
|
||||
<input type="password" id="login-screen-password" class="form-input" placeholder="Введите мастер-пароль..." autocomplete="current-password" required>
|
||||
<button type="button" id="btn-toggle-login-password" class="btn-toggle-pwd" title="Показать/скрыть пароль" tabindex="-1">👁️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="login-screen-error" class="login-error-alert" style="display: none;"></div>
|
||||
<button type="submit" id="btn-login-screen-submit" class="btn btn-primary btn-block">Войти в систему 🔐</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Configurator Application -->
|
||||
<div id="configurator-app-container" style="display: none;">
|
||||
<div class="app-container">
|
||||
<!-- Header -->
|
||||
<header class="app-header">
|
||||
<div class="brand">
|
||||
@@ -900,6 +924,7 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL: LOGIN -->
|
||||
<div id="modal-login" class="modal-overlay">
|
||||
|
||||
@@ -33,11 +33,9 @@ class PvZConfiguratorApp {
|
||||
this.onAuthenticated();
|
||||
} else {
|
||||
this.onUnauthenticated();
|
||||
this.loadPublicConfig();
|
||||
}
|
||||
} else {
|
||||
this.onUnauthenticated();
|
||||
this.loadPublicConfig();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,24 +101,116 @@ class PvZConfiguratorApp {
|
||||
|
||||
onAuthenticated() {
|
||||
this.isAuthenticated = true;
|
||||
document.getElementById('admin-status-dot').className = 'status-dot';
|
||||
document.getElementById('admin-status-text').textContent = 'Администратор';
|
||||
document.getElementById('btn-login-modal-open').style.display = 'none';
|
||||
|
||||
const loginScreen = document.getElementById('configurator-login-screen');
|
||||
if (loginScreen) loginScreen.style.display = 'none';
|
||||
|
||||
const appContainer = document.getElementById('configurator-app-container');
|
||||
if (appContainer) appContainer.style.display = 'block';
|
||||
|
||||
const dot = document.getElementById('admin-status-dot');
|
||||
if (dot) dot.className = 'status-dot';
|
||||
const stText = document.getElementById('admin-status-text');
|
||||
if (stText) stText.textContent = 'Администратор';
|
||||
|
||||
const btnLoginModal = document.getElementById('btn-login-modal-open');
|
||||
if (btnLoginModal) btnLoginModal.style.display = 'none';
|
||||
|
||||
const btnPwd = document.getElementById('btn-admin-password-modal-open');
|
||||
if (btnPwd) btnPwd.style.display = 'inline-flex';
|
||||
document.getElementById('btn-admin-logout').style.display = 'inline-flex';
|
||||
|
||||
const btnLogout = document.getElementById('btn-admin-logout');
|
||||
if (btnLogout) btnLogout.style.display = 'inline-flex';
|
||||
|
||||
this.loadAllData();
|
||||
}
|
||||
|
||||
onUnauthenticated() {
|
||||
this.isAuthenticated = false;
|
||||
document.getElementById('admin-status-dot').className = 'status-dot offline';
|
||||
document.getElementById('admin-status-text').textContent = 'Гость (Только чтение)';
|
||||
document.getElementById('btn-login-modal-open').style.display = 'inline-flex';
|
||||
this.token = '';
|
||||
localStorage.removeItem('pvz_admin_token');
|
||||
this.config = null;
|
||||
this.stats = null;
|
||||
this.users = [];
|
||||
this.sessions = [];
|
||||
this.loginLogs = [];
|
||||
this.bugs = [];
|
||||
this.levels = [];
|
||||
|
||||
const appContainer = document.getElementById('configurator-app-container');
|
||||
if (appContainer) appContainer.style.display = 'none';
|
||||
|
||||
const loginScreen = document.getElementById('configurator-login-screen');
|
||||
if (loginScreen) loginScreen.style.display = 'flex';
|
||||
|
||||
const loginErr = document.getElementById('login-screen-error');
|
||||
if (loginErr) {
|
||||
loginErr.style.display = 'none';
|
||||
loginErr.textContent = '';
|
||||
}
|
||||
|
||||
const pwdInput = document.getElementById('login-screen-password');
|
||||
if (pwdInput) pwdInput.value = '';
|
||||
|
||||
const dot = document.getElementById('admin-status-dot');
|
||||
if (dot) dot.className = 'status-dot offline';
|
||||
const stText = document.getElementById('admin-status-text');
|
||||
if (stText) stText.textContent = 'Гость (Только чтение)';
|
||||
|
||||
const btnLoginModal = document.getElementById('btn-login-modal-open');
|
||||
if (btnLoginModal) btnLoginModal.style.display = 'inline-flex';
|
||||
|
||||
const btnPwd = document.getElementById('btn-admin-password-modal-open');
|
||||
if (btnPwd) btnPwd.style.display = 'none';
|
||||
document.getElementById('btn-admin-logout').style.display = 'none';
|
||||
|
||||
const btnLogout = document.getElementById('btn-admin-logout');
|
||||
if (btnLogout) btnLogout.style.display = 'none';
|
||||
}
|
||||
|
||||
openModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.add('active', 'open');
|
||||
}
|
||||
}
|
||||
|
||||
closeModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.remove('active', 'open');
|
||||
}
|
||||
}
|
||||
|
||||
async handleScreenLogin() {
|
||||
const pwdInput = document.getElementById('login-screen-password');
|
||||
const errDiv = document.getElementById('login-screen-error');
|
||||
const enteredPassword = pwdInput ? pwdInput.value.trim() : '';
|
||||
|
||||
if (!enteredPassword) {
|
||||
if (errDiv) {
|
||||
errDiv.textContent = 'Введите мастер-пароль администратора';
|
||||
errDiv.style.display = 'flex';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (errDiv) {
|
||||
errDiv.style.display = 'none';
|
||||
errDiv.textContent = '';
|
||||
}
|
||||
|
||||
const res = await this.apiRequest('/api/admin/auth', 'POST', { password: enteredPassword });
|
||||
if (res.ok && res.data && res.data.success && res.data.token) {
|
||||
this.token = res.data.token;
|
||||
localStorage.setItem('pvz_admin_token', this.token);
|
||||
this.showToast('Авторизация успешна! Добро пожаловать в панель управления.', 'success');
|
||||
this.onAuthenticated();
|
||||
} else {
|
||||
if (errDiv) {
|
||||
errDiv.textContent = (res.data && res.data.message) || 'Неверный пароль администратора';
|
||||
errDiv.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadPublicConfig() {
|
||||
@@ -746,20 +836,24 @@ class PvZConfiguratorApp {
|
||||
filtered.forEach(s => {
|
||||
const tr = document.createElement('tr');
|
||||
const tokenShort = (s.token || '').substring(0, 10) + '...';
|
||||
const isOnline = true;
|
||||
const username = s.username || 'Гость';
|
||||
const ipAddress = s.ipAddress || '127.0.0.1';
|
||||
const deviceName = (s.deviceName && s.deviceName !== 'Unknown Device') ? s.deviceName : 'Браузер';
|
||||
const createdAt = s.createdAt || '—';
|
||||
const lastActive = s.lastActive || s.createdAt || 'Только что';
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<span class="player-name-link" data-user-id="${s.userId}">${s.username || 'Гость'}</span>
|
||||
<span class="player-name-link" data-user-id="${s.userId || ''}">${username}</span>
|
||||
${s.isAdmin ? '<span class="badge badge-warning" style="font-size:10px; margin-left:4px;">ADMIN</span>' : ''}
|
||||
</td>
|
||||
<td><span style="font-family:monospace; color:var(--text-muted);">${s.ipAddress || '127.0.0.1'}</span></td>
|
||||
<td><span style="color:var(--text-main); font-size:12px;">${s.deviceName || 'Браузер'}</span></td>
|
||||
<td style="color:var(--text-dim); font-size:12px;">${s.createdAt || '—'}</td>
|
||||
<td><span style="color:#a5d6a7; font-size:12px;">🟢 ${s.lastActive || s.createdAt || 'Только что'}</span></td>
|
||||
<td><span class="token-cell" title="${s.token}">${tokenShort}</span></td>
|
||||
<td><span style="font-family:monospace; color:var(--text-muted); font-size:12px;">${ipAddress}</span></td>
|
||||
<td><span style="color:var(--text-main); font-size:12px;">${deviceName}</span></td>
|
||||
<td style="color:var(--text-dim); font-size:12px;">${createdAt}</td>
|
||||
<td><span style="color:#a5d6a7; font-size:12px;">🟢 ${lastActive}</span></td>
|
||||
<td><span class="token-cell" title="${s.token || ''}">${tokenShort}</span></td>
|
||||
<td>
|
||||
<button class="btn btn-danger btn-sm btn-terminate-session" data-token="${s.token}" title="Завершить сессию игрока">
|
||||
<button class="btn btn-danger btn-sm btn-terminate-session" data-token="${s.token || ''}" title="Завершить сессию игрока">
|
||||
🚪 Завершить
|
||||
</button>
|
||||
</td>
|
||||
@@ -825,7 +919,10 @@ class PvZConfiguratorApp {
|
||||
filtered = filtered.filter(l => l.username && l.username.toLowerCase().includes(userFilter));
|
||||
}
|
||||
if (ipFilter) {
|
||||
filtered = filtered.filter(l => l.ipAddress && l.ipAddress.toLowerCase().includes(ipFilter));
|
||||
filtered = filtered.filter(l =>
|
||||
(l.ipAddress && l.ipAddress.toLowerCase().includes(ipFilter)) ||
|
||||
(l.ip && l.ip.toLowerCase().includes(ipFilter))
|
||||
);
|
||||
}
|
||||
if (statusFilter !== 'all') {
|
||||
filtered = filtered.filter(l => l.status === statusFilter);
|
||||
@@ -839,27 +936,32 @@ class PvZConfiguratorApp {
|
||||
tbody.innerHTML = '';
|
||||
filtered.forEach(log => {
|
||||
const tr = document.createElement('tr');
|
||||
const timestamp = log.createdAt || log.timestamp || '—';
|
||||
const username = log.username || 'Гость';
|
||||
const ipAddress = log.ipAddress || log.ip || '127.0.0.1';
|
||||
const deviceName = (log.deviceName && log.deviceName !== 'Unknown Device') ? log.deviceName : 'Браузер';
|
||||
const status = log.status || 'unknown';
|
||||
|
||||
let statusBadge = `<span class="badge badge-muted">${log.status}</span>`;
|
||||
if (log.status === 'success') {
|
||||
let statusBadge = `<span class="badge badge-muted">${status}</span>`;
|
||||
if (status === 'success') {
|
||||
statusBadge = `<span class="badge badge-success">✅ Успешно</span>`;
|
||||
} else if (log.status === 'registered') {
|
||||
} else if (status === 'registered') {
|
||||
statusBadge = `<span class="badge badge-purple">🎉 Регистрация</span>`;
|
||||
} else if (log.status === 'wrong_password') {
|
||||
} else if (status === 'wrong_password') {
|
||||
statusBadge = `<span class="badge badge-danger">❌ Неверный пароль</span>`;
|
||||
} else if (log.status === 'banned_login_attempt') {
|
||||
} else if (status === 'banned_login_attempt') {
|
||||
statusBadge = `<span class="badge badge-warning">🚫 Заблокирован</span>`;
|
||||
} else if (log.status === 'rate_limited') {
|
||||
} else if (status === 'rate_limited') {
|
||||
statusBadge = `<span class="badge badge-warning">⚠️ Превышен лимит</span>`;
|
||||
} else if (log.status === 'user_not_found') {
|
||||
} else if (status === 'user_not_found') {
|
||||
statusBadge = `<span class="badge badge-muted">❓ Не найден</span>`;
|
||||
}
|
||||
|
||||
tr.innerHTML = `
|
||||
<td style="color:var(--text-dim); font-size:12px; font-family:monospace;">${log.timestamp || '—'}</td>
|
||||
<td><b style="color:var(--accent-cyan);">${log.username || '—'}</b></td>
|
||||
<td><span style="font-family:monospace; color:var(--text-muted); font-size:12px;">${log.ipAddress || '—'}</span></td>
|
||||
<td style="font-size:12px; color:var(--text-main);">${log.deviceName || 'Браузер'}</td>
|
||||
<td style="color:var(--text-dim); font-size:12px; font-family:monospace;">${timestamp}</td>
|
||||
<td><b style="color:var(--accent-cyan);">${username}</b></td>
|
||||
<td><span style="font-family:monospace; color:var(--text-muted); font-size:12px;">${ipAddress}</span></td>
|
||||
<td style="font-size:12px; color:var(--text-main);">${deviceName}</td>
|
||||
<td>${statusBadge}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
@@ -902,7 +1004,7 @@ class PvZConfiguratorApp {
|
||||
|
||||
if (res.ok && res.data.success) {
|
||||
this.showToast('Мастер-пароль администратора успешно изменен!', 'success');
|
||||
document.getElementById('modal-admin-password').classList.remove('active');
|
||||
this.closeModal('modal-admin-password');
|
||||
document.getElementById('admin-current-password').value = '';
|
||||
document.getElementById('admin-new-password').value = '';
|
||||
document.getElementById('admin-confirm-password').value = '';
|
||||
@@ -987,8 +1089,13 @@ class PvZConfiguratorApp {
|
||||
}
|
||||
|
||||
async openPlayerStatsModal(userId) {
|
||||
if (!this.isAuthenticated) {
|
||||
this.showToast('Требуется авторизация администратора', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await this.apiRequest(`/api/admin/user/${userId}`);
|
||||
if (!res.ok || !res.data.success) {
|
||||
if (!res.ok || !res.data || !res.data.success) {
|
||||
this.showToast('Ошибка загрузки статистики игрока', 'error');
|
||||
return;
|
||||
}
|
||||
@@ -1003,7 +1110,7 @@ class PvZConfiguratorApp {
|
||||
const idInput = document.getElementById('stats-player-id');
|
||||
if (idInput) idInput.value = u.id;
|
||||
|
||||
document.getElementById('stats-player-username').textContent = u.username;
|
||||
document.getElementById('stats-player-username').textContent = u.username || '—';
|
||||
document.getElementById('stats-player-id-line').textContent = `#${u.id} (${prof.theme || 'classic'})`;
|
||||
document.getElementById('stats-player-created').textContent = u.createdAt || '—';
|
||||
document.getElementById('stats-player-login').textContent = u.lastLogin || '—';
|
||||
@@ -1041,12 +1148,17 @@ class PvZConfiguratorApp {
|
||||
}
|
||||
document.getElementById('stats-scores-summary').innerHTML = scoreTxt;
|
||||
|
||||
document.getElementById('modal-player-stats').classList.add('active');
|
||||
this.openModal('modal-player-stats');
|
||||
}
|
||||
|
||||
async openEditPlayerModal(userId) {
|
||||
if (!this.isAuthenticated) {
|
||||
this.showToast('Требуется авторизация администратора', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await this.apiRequest(`/api/admin/user/${userId}`);
|
||||
if (!res.ok || !res.data.success) {
|
||||
if (!res.ok || !res.data || !res.data.success) {
|
||||
this.showToast('Ошибка загрузки профиля игрока', 'error');
|
||||
return;
|
||||
}
|
||||
@@ -1057,7 +1169,7 @@ class PvZConfiguratorApp {
|
||||
const zen = prof.zenGarden || u.zenGarden || {};
|
||||
|
||||
document.getElementById('edit-player-id').value = u.id;
|
||||
document.getElementById('edit-player-title-name').textContent = u.username;
|
||||
document.getElementById('edit-player-title-name').textContent = u.username || '';
|
||||
document.getElementById('edit-player-coins').value = prof.coins ?? u.coins ?? 150;
|
||||
document.getElementById('edit-player-level').value = prof.level ?? u.level ?? 1;
|
||||
document.getElementById('edit-player-maxlevel').value = prof.maxUnlockedLevel ?? u.maxUnlockedLevel ?? 1;
|
||||
@@ -1081,10 +1193,15 @@ class PvZConfiguratorApp {
|
||||
const banCont = document.getElementById('edit-ban-reason-container');
|
||||
if (banCont) banCont.style.display = u.isBanned ? 'block' : 'none';
|
||||
|
||||
document.getElementById('modal-edit-player').classList.add('active');
|
||||
this.openModal('modal-edit-player');
|
||||
}
|
||||
|
||||
async savePlayerEdit() {
|
||||
if (!this.isAuthenticated) {
|
||||
this.showToast('Требуется авторизация администратора', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const id = document.getElementById('edit-player-id').value;
|
||||
const updates = {
|
||||
coins: parseInt(document.getElementById('edit-player-coins').value, 10) || 0,
|
||||
@@ -1113,17 +1230,22 @@ class PvZConfiguratorApp {
|
||||
};
|
||||
|
||||
const res = await this.apiRequest(`/api/admin/user/${id}/edit`, 'POST', { updates });
|
||||
if (res.ok && res.data.success) {
|
||||
if (res.ok && res.data && res.data.success) {
|
||||
this.showToast('Профиль игрока и ресурсы успешно обновлены!', 'success');
|
||||
document.getElementById('modal-edit-player').classList.remove('active');
|
||||
this.closeModal('modal-edit-player');
|
||||
this.loadUsers();
|
||||
this.loadStats();
|
||||
} else {
|
||||
this.showToast(res.data.message || 'Ошибка сохранения', 'error');
|
||||
this.showToast((res.data && res.data.message) || 'Ошибка сохранения', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async createPlayer() {
|
||||
if (!this.isAuthenticated) {
|
||||
this.showToast('Требуется авторизация администратора', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const username = document.getElementById('create-player-username').value.trim();
|
||||
const password = document.getElementById('create-player-password').value.trim();
|
||||
const coins = parseInt(document.getElementById('create-player-coins').value, 10) || 150;
|
||||
@@ -1141,26 +1263,36 @@ class PvZConfiguratorApp {
|
||||
initialData: { coins, level: 1, maxUnlockedLevel: 1 }
|
||||
});
|
||||
|
||||
if (res.ok && res.data.success) {
|
||||
this.showToast(`Игрок ${username} создан!`, 'success');
|
||||
document.getElementById('modal-create-player').classList.remove('active');
|
||||
if (res.ok && res.data && res.data.success) {
|
||||
this.showToast(`Игрок ${username} успешно создан!`, 'success');
|
||||
this.closeModal('modal-create-player');
|
||||
document.getElementById('create-player-username').value = '';
|
||||
document.getElementById('create-player-password').value = '';
|
||||
this.loadUsers();
|
||||
this.loadStats();
|
||||
} else {
|
||||
this.showToast(res.data.message || 'Ошибка создания игрока', 'error');
|
||||
this.showToast((res.data && res.data.message) || 'Ошибка создания игрока', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
openResetPasswordModal(id, username) {
|
||||
if (!this.isAuthenticated) {
|
||||
this.showToast('Требуется авторизация администратора', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('reset-pwd-user-id').value = id;
|
||||
document.getElementById('reset-pwd-username').textContent = username;
|
||||
document.getElementById('reset-pwd-input').value = '';
|
||||
document.getElementById('modal-reset-password').classList.add('active');
|
||||
this.openModal('modal-reset-password');
|
||||
}
|
||||
|
||||
async submitResetPassword() {
|
||||
if (!this.isAuthenticated) {
|
||||
this.showToast('Требуется авторизация администратора', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const id = document.getElementById('reset-pwd-user-id').value;
|
||||
const pwd = document.getElementById('reset-pwd-input').value.trim();
|
||||
if (!pwd) {
|
||||
@@ -1169,11 +1301,11 @@ class PvZConfiguratorApp {
|
||||
}
|
||||
|
||||
const res = await this.apiRequest(`/api/admin/user/${id}/password`, 'POST', { password: pwd });
|
||||
if (res.ok && res.data.success) {
|
||||
if (res.ok && res.data && res.data.success) {
|
||||
this.showToast('Пароль успешно сброшен', 'success');
|
||||
document.getElementById('modal-reset-password').classList.remove('active');
|
||||
this.closeModal('modal-reset-password');
|
||||
} else {
|
||||
this.showToast(res.data.message || 'Ошибка сброса пароля', 'error');
|
||||
this.showToast((res.data && res.data.message) || 'Ошибка сброса пароля', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1254,7 +1386,7 @@ class PvZConfiguratorApp {
|
||||
document.getElementById('bug-mod-status').value = b.status || 'новый';
|
||||
document.getElementById('bug-mod-reply').value = b.adminReply || '';
|
||||
|
||||
document.getElementById('modal-moderate-bug').classList.add('active');
|
||||
this.openModal('modal-moderate-bug');
|
||||
}
|
||||
|
||||
async saveBugModeration() {
|
||||
@@ -1265,7 +1397,7 @@ class PvZConfiguratorApp {
|
||||
const res = await this.apiRequest(`/api/admin/bugs/${id}/status`, 'POST', { status, adminReply });
|
||||
if (res.ok && res.data.success) {
|
||||
this.showToast(`Баг #${id} обновлен`, 'success');
|
||||
document.getElementById('modal-moderate-bug').classList.remove('active');
|
||||
this.closeModal('modal-moderate-bug');
|
||||
this.loadBugs();
|
||||
} else {
|
||||
this.showToast(res.data.message || 'Ошибка сохранения', 'error');
|
||||
@@ -1279,7 +1411,7 @@ class PvZConfiguratorApp {
|
||||
const res = await this.apiRequest(`/api/admin/bugs/${id}/status`, 'POST', { delete: true });
|
||||
if (res.ok && res.data.success) {
|
||||
this.showToast(`Баг #${id} удален`, 'success');
|
||||
document.getElementById('modal-moderate-bug').classList.remove('active');
|
||||
this.closeModal('modal-moderate-bug');
|
||||
this.loadBugs();
|
||||
} else {
|
||||
this.showToast(res.data.message || 'Ошибка удаления', 'error');
|
||||
@@ -1434,46 +1566,94 @@ class PvZConfiguratorApp {
|
||||
if (pane) pane.classList.add('active');
|
||||
this.activeTab = target;
|
||||
|
||||
if (target === 'tab-levels' && this.levels.length === 0) this.loadCampaignLevels();
|
||||
if (target === 'tab-players') this.loadUsers();
|
||||
if (target === 'tab-sessions') this.loadSessions();
|
||||
if (target === 'tab-logs') this.loadLoginLogs();
|
||||
if (target === 'tab-levels' && this.levels.length === 0) this.loadCampaignLevels();
|
||||
});
|
||||
});
|
||||
|
||||
// Full-Screen Login Screen Bindings
|
||||
const btnScreenSubmit = document.getElementById('btn-login-screen-submit');
|
||||
if (btnScreenSubmit) {
|
||||
btnScreenSubmit.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.handleScreenLogin();
|
||||
});
|
||||
}
|
||||
|
||||
const screenPwdInput = document.getElementById('login-screen-password');
|
||||
if (screenPwdInput) {
|
||||
screenPwdInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.handleScreenLogin();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const btnTogglePwd = document.getElementById('btn-toggle-login-password');
|
||||
if (btnTogglePwd && screenPwdInput) {
|
||||
btnTogglePwd.addEventListener('click', () => {
|
||||
if (screenPwdInput.type === 'password') {
|
||||
screenPwdInput.type = 'text';
|
||||
btnTogglePwd.textContent = '🙈';
|
||||
} else {
|
||||
screenPwdInput.type = 'password';
|
||||
btnTogglePwd.textContent = '👁️';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Login modal
|
||||
document.getElementById('btn-login-modal-open').addEventListener('click', () => {
|
||||
document.getElementById('login-error-msg').style.display = 'none';
|
||||
document.getElementById('admin-password-input').value = '';
|
||||
document.getElementById('modal-login').classList.add('active');
|
||||
});
|
||||
|
||||
document.getElementById('btn-submit-login').addEventListener('click', async () => {
|
||||
const pwd = document.getElementById('admin-password-input').value.trim();
|
||||
if (!pwd) return;
|
||||
|
||||
const res = await this.apiRequest('/api/admin/auth', 'POST', { password: pwd });
|
||||
if (res.ok && res.data.success) {
|
||||
this.token = res.data.token;
|
||||
localStorage.setItem('pvz_admin_token', this.token);
|
||||
this.onAuthenticated();
|
||||
document.getElementById('modal-login').classList.remove('active');
|
||||
this.showToast('Авторизация администратора успешна!', 'success');
|
||||
} else {
|
||||
const btnLoginOpen = document.getElementById('btn-login-modal-open');
|
||||
if (btnLoginOpen) {
|
||||
btnLoginOpen.addEventListener('click', () => {
|
||||
const err = document.getElementById('login-error-msg');
|
||||
err.textContent = res.data.message || 'Неверный пароль администратора';
|
||||
err.style.display = 'block';
|
||||
}
|
||||
});
|
||||
if (err) err.style.display = 'none';
|
||||
const input = document.getElementById('admin-password-input');
|
||||
if (input) input.value = '';
|
||||
this.openModal('modal-login');
|
||||
});
|
||||
}
|
||||
|
||||
const btnSubmitLogin = document.getElementById('btn-submit-login');
|
||||
if (btnSubmitLogin) {
|
||||
btnSubmitLogin.addEventListener('click', async () => {
|
||||
const input = document.getElementById('admin-password-input');
|
||||
const pwd = input ? input.value.trim() : '';
|
||||
if (!pwd) return;
|
||||
|
||||
const res = await this.apiRequest('/api/admin/auth', 'POST', { password: pwd });
|
||||
if (res.ok && res.data && res.data.success && res.data.token) {
|
||||
this.token = res.data.token;
|
||||
localStorage.setItem('pvz_admin_token', this.token);
|
||||
this.closeModal('modal-login');
|
||||
this.showToast('Авторизация администратора успешна!', 'success');
|
||||
this.onAuthenticated();
|
||||
} else {
|
||||
const err = document.getElementById('login-error-msg');
|
||||
if (err) {
|
||||
err.textContent = (res.data && res.data.message) || 'Неверный пароль администратора';
|
||||
err.style.display = 'block';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Change Admin Password Modal
|
||||
const btnOpenPwd = document.getElementById('btn-admin-password-modal-open');
|
||||
if (btnOpenPwd) {
|
||||
btnOpenPwd.addEventListener('click', () => {
|
||||
document.getElementById('admin-password-error').style.display = 'none';
|
||||
document.getElementById('admin-current-password').value = '';
|
||||
document.getElementById('admin-new-password').value = '';
|
||||
document.getElementById('admin-confirm-password').value = '';
|
||||
document.getElementById('modal-admin-password').classList.add('active');
|
||||
const err = document.getElementById('admin-password-error');
|
||||
if (err) err.style.display = 'none';
|
||||
const cur = document.getElementById('admin-current-password');
|
||||
if (cur) cur.value = '';
|
||||
const nw = document.getElementById('admin-new-password');
|
||||
if (nw) nw.value = '';
|
||||
const cf = document.getElementById('admin-confirm-password');
|
||||
if (cf) cf.value = '';
|
||||
this.openModal('modal-admin-password');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1483,19 +1663,29 @@ class PvZConfiguratorApp {
|
||||
}
|
||||
|
||||
// Logout
|
||||
document.getElementById('btn-admin-logout').addEventListener('click', () => {
|
||||
this.token = '';
|
||||
localStorage.removeItem('pvz_admin_token');
|
||||
this.onUnauthenticated();
|
||||
this.showToast('Вы вышли из панели администратора', 'info');
|
||||
});
|
||||
const btnLogout = document.getElementById('btn-admin-logout');
|
||||
if (btnLogout) {
|
||||
btnLogout.addEventListener('click', () => {
|
||||
this.token = '';
|
||||
localStorage.removeItem('pvz_admin_token');
|
||||
this.onUnauthenticated();
|
||||
this.showToast('Вы вышли из панели администратора', 'info');
|
||||
});
|
||||
}
|
||||
|
||||
// Close Modals
|
||||
document.querySelectorAll('[data-close]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const modalId = btn.getAttribute('data-close');
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) modal.classList.remove('active');
|
||||
this.closeModal(modalId);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) {
|
||||
overlay.classList.remove('active', 'open');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1539,26 +1729,56 @@ class PvZConfiguratorApp {
|
||||
});
|
||||
|
||||
// Player Actions
|
||||
document.getElementById('btn-create-player-open').addEventListener('click', () => {
|
||||
document.getElementById('modal-create-player').classList.add('active');
|
||||
});
|
||||
document.getElementById('btn-submit-create-player').addEventListener('click', () => this.createPlayer());
|
||||
document.getElementById('btn-save-player-edit').addEventListener('click', () => this.savePlayerEdit());
|
||||
document.getElementById('btn-submit-reset-pwd').addEventListener('click', () => this.submitResetPassword());
|
||||
document.getElementById('btn-refresh-players').addEventListener('click', () => this.loadUsers());
|
||||
document.getElementById('player-search-input').addEventListener('input', () => this.renderPlayersTable());
|
||||
document.getElementById('player-filter-status').addEventListener('change', () => this.renderPlayersTable());
|
||||
document.getElementById('player-sort-select').addEventListener('change', () => this.renderPlayersTable());
|
||||
document.getElementById('edit-player-is-banned').addEventListener('change', (e) => {
|
||||
document.getElementById('edit-ban-reason-container').style.display = e.target.checked ? 'block' : 'none';
|
||||
});
|
||||
const btnCreatePlayerOpen = document.getElementById('btn-create-player-open');
|
||||
if (btnCreatePlayerOpen) {
|
||||
btnCreatePlayerOpen.addEventListener('click', () => {
|
||||
this.openModal('modal-create-player');
|
||||
});
|
||||
}
|
||||
const btnSubmitCreatePlayer = document.getElementById('btn-submit-create-player');
|
||||
if (btnSubmitCreatePlayer) {
|
||||
btnSubmitCreatePlayer.addEventListener('click', () => this.createPlayer());
|
||||
}
|
||||
const btnSavePlayerEdit = document.getElementById('btn-save-player-edit');
|
||||
if (btnSavePlayerEdit) {
|
||||
btnSavePlayerEdit.addEventListener('click', () => this.savePlayerEdit());
|
||||
}
|
||||
const btnSubmitResetPwd = document.getElementById('btn-submit-reset-pwd');
|
||||
if (btnSubmitResetPwd) {
|
||||
btnSubmitResetPwd.addEventListener('click', () => this.submitResetPassword());
|
||||
}
|
||||
const btnRefreshPlayers = document.getElementById('btn-refresh-players');
|
||||
if (btnRefreshPlayers) {
|
||||
btnRefreshPlayers.addEventListener('click', () => this.loadUsers());
|
||||
}
|
||||
const playerSearchInput = document.getElementById('player-search-input');
|
||||
if (playerSearchInput) {
|
||||
playerSearchInput.addEventListener('input', () => this.renderPlayersTable());
|
||||
}
|
||||
const playerFilterStatus = document.getElementById('player-filter-status');
|
||||
if (playerFilterStatus) {
|
||||
playerFilterStatus.addEventListener('change', () => this.renderPlayersTable());
|
||||
}
|
||||
const playerSortSelect = document.getElementById('player-sort-select');
|
||||
if (playerSortSelect) {
|
||||
playerSortSelect.addEventListener('change', () => this.renderPlayersTable());
|
||||
}
|
||||
const editPlayerIsBanned = document.getElementById('edit-player-is-banned');
|
||||
if (editPlayerIsBanned) {
|
||||
editPlayerIsBanned.addEventListener('change', (e) => {
|
||||
const container = document.getElementById('edit-ban-reason-container');
|
||||
if (container) container.style.display = e.target.checked ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('btn-stats-jump-edit')?.addEventListener('click', () => {
|
||||
const id = document.getElementById('stats-player-id')?.value;
|
||||
const modal = document.getElementById('modal-player-stats');
|
||||
if (modal) modal.classList.remove('active');
|
||||
if (id) this.openEditPlayerModal(id);
|
||||
});
|
||||
const btnStatsJumpEdit = document.getElementById('btn-stats-jump-edit');
|
||||
if (btnStatsJumpEdit) {
|
||||
btnStatsJumpEdit.addEventListener('click', () => {
|
||||
const id = document.getElementById('stats-player-id')?.value;
|
||||
this.closeModal('modal-player-stats');
|
||||
if (id) this.openEditPlayerModal(id);
|
||||
});
|
||||
}
|
||||
|
||||
// Balance Save Buttons
|
||||
document.getElementById('btn-save-economy').addEventListener('click', () => this.saveBalanceSection('economy'));
|
||||
|
||||
Reference in new issue
Block a user