admin_to_react
This commit is contained in:
@@ -0,0 +1,868 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const statusElement = document.getElementById('server-status');
|
||||
const refreshBtn = document.getElementById('refresh-status-btn');
|
||||
const logsElement = document.getElementById('logs-content');
|
||||
const configForm = document.getElementById('config-form');
|
||||
const payslipsPathInput = document.getElementById('payslipsPath');
|
||||
const configStatus = document.getElementById('config-status');
|
||||
const browseBtn = document.getElementById('browse-btn');
|
||||
const dbStatusElement = document.getElementById('db-status');
|
||||
const checkDbBtn = document.getElementById('check-db-btn');
|
||||
const refreshLogsBtn = document.getElementById('refresh-logs-btn');
|
||||
const clearLogsBtn = document.getElementById('clear-logs-btn');
|
||||
const adminDbStatusEl = document.getElementById('admin-db-status');
|
||||
const adminDbMessageEl = document.getElementById('admin-db-message');
|
||||
const backendDbStatusEl = document.getElementById('backend-db-status');
|
||||
const backendDbMessageEl = document.getElementById('backend-db-message');
|
||||
const pingBtn = document.getElementById('ping-btn');
|
||||
const pingStopBtn = document.getElementById('ping-stop-btn');
|
||||
const pingClearBtn = document.getElementById('ping-clear-btn');
|
||||
const pingIpInput = document.getElementById('ping-ip');
|
||||
const pingOutputEl = document.getElementById('ping-output');
|
||||
let ws;
|
||||
|
||||
const loginForm = document.getElementById('login-form');
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const password = loginForm.elements['password'].value;
|
||||
try {
|
||||
const response = await fetch('/api/auth', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password })
|
||||
});
|
||||
if (response.ok) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
alert('Неверный пароль');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Ошибка авторизации');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fetchStatus() {
|
||||
fetch('/api/status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'online') {
|
||||
statusElement.textContent = 'Онлайн';
|
||||
statusElement.style.color = 'green';
|
||||
} else {
|
||||
statusElement.textContent = 'Оффлайн';
|
||||
statusElement.style.color = 'red';
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Ошибка при получении статуса:', err);
|
||||
statusElement.textContent = 'Ошибка';
|
||||
statusElement.style.color = 'orange';
|
||||
});
|
||||
}
|
||||
|
||||
refreshBtn.addEventListener('click', fetchStatus);
|
||||
function fetchLogs() {
|
||||
logsElement.textContent = 'загрузка...';
|
||||
fetch('/api/logs')
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Не удалось загрузить логи');
|
||||
return response.text();
|
||||
})
|
||||
.then(data => {
|
||||
logsElement.textContent = data || 'Лог-файл пуст.';
|
||||
})
|
||||
.catch(err => {
|
||||
logsElement.textContent = `Ошибка при загрузке логов: ${err.message}`;
|
||||
logsElement.style.color = '#ff6b6b';
|
||||
});
|
||||
}
|
||||
function fetchConfig() {
|
||||
fetch('/api/config')
|
||||
.then(response => response.json())
|
||||
.then(config => {
|
||||
payslipsPathInput.value = config.payslipsPath;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Ошибка при получении конфигурации:', err);
|
||||
configStatus.textContent = 'Не удалось загрузить конфигурацию.';
|
||||
configStatus.style.color = 'red';
|
||||
});
|
||||
}
|
||||
|
||||
configForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const newConfig = {
|
||||
payslipsPath: payslipsPathInput.value
|
||||
};
|
||||
|
||||
apiFetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(newConfig)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
configStatus.textContent = 'Конфигурация успешно сохранена!';
|
||||
configStatus.style.color = 'green';
|
||||
setTimeout(() => { configStatus.textContent = ''; }, 3000);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Ошибка при сохранении конфигурации:', err);
|
||||
configStatus.textContent = 'Ошибка при сохранении.';
|
||||
configStatus.style.color = 'red';
|
||||
});
|
||||
});
|
||||
browseBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
const response = await fetch('/api/browse');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
payslipsPathInput.value = data.path;
|
||||
} else {
|
||||
console.info('Folder selection was canceled.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error opening folder dialog:', error);
|
||||
alert('Не удалось открыть диалог выбора папки.');
|
||||
}
|
||||
});
|
||||
|
||||
function renderStatus(element, messageEl, data) {
|
||||
messageEl.textContent = data.message;
|
||||
if (data.status === 'online') {
|
||||
element.textContent = 'Онлайн';
|
||||
element.style.color = 'green';
|
||||
} else {
|
||||
element.textContent = 'Оффлайн';
|
||||
element.style.color = 'red';
|
||||
}
|
||||
}
|
||||
function checkDbStatus() {
|
||||
adminDbStatusEl.textContent = 'проверка...';
|
||||
backendDbStatusEl.textContent = 'проверка...';
|
||||
adminDbMessageEl.textContent = '';
|
||||
backendDbMessageEl.textContent = '';
|
||||
adminDbStatusEl.style.color = '#333';
|
||||
backendDbStatusEl.style.color = '#333';
|
||||
|
||||
fetch('/api/health/check-db')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
renderStatus(adminDbStatusEl, adminDbMessageEl, data.adminToDb);
|
||||
renderStatus(backendDbStatusEl, backendDbMessageEl, data.backendToDb);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Ошибка при проверке статуса БД:', err);
|
||||
adminDbStatusEl.textContent = 'Ошибка';
|
||||
adminDbStatusEl.style.color = 'orange';
|
||||
backendDbStatusEl.textContent = 'Ошибка';
|
||||
backendDbStatusEl.style.color = 'orange';
|
||||
});
|
||||
}
|
||||
|
||||
checkDbBtn.addEventListener('click', checkDbStatus);
|
||||
|
||||
function clearLogs() {
|
||||
if (!confirm('Это действие удалит лог-файл полностью и навсегда. Вы уверены?')) {
|
||||
return;
|
||||
}
|
||||
fetch('/api/logs/clear', { method: 'POST' })
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
fetchLogs();
|
||||
} else {
|
||||
alert('Не удалось очистить логи.');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Ошибка при очистке логов:', err);
|
||||
alert('Произошла ошибка при очистке логов.');
|
||||
});
|
||||
}
|
||||
|
||||
refreshLogsBtn.addEventListener('click', fetchLogs);
|
||||
clearLogsBtn.addEventListener('click', clearLogs);
|
||||
|
||||
function doPing() {
|
||||
const ip = pingIpInput.value.trim();
|
||||
if (!ip) {
|
||||
pingOutputEl.textContent = 'Пожалуйста, введите IP-адрес.';
|
||||
return;
|
||||
}
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
ws = new WebSocket(`${wsProtocol}://${window.location.host}`);
|
||||
|
||||
ws.onopen = () => {
|
||||
pingOutputEl.textContent = '';
|
||||
pingBtn.disabled = true;
|
||||
pingStopBtn.disabled = false;
|
||||
ws.send(JSON.stringify({ type: 'ping', ip }));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
pingOutputEl.textContent += message.data;
|
||||
pingOutputEl.scrollTop = pingOutputEl.scrollHeight;
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket Error:', error);
|
||||
pingOutputEl.textContent += '\nОшибка WebSocket соединения.';
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.info('WebSocket connection closed');
|
||||
pingBtn.disabled = false;
|
||||
pingStopBtn.disabled = true;
|
||||
ws = null;
|
||||
};
|
||||
}
|
||||
|
||||
function stopPing() {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
function clearPingOutput() {
|
||||
pingOutputEl.textContent = '';
|
||||
}
|
||||
pingBtn.addEventListener('click', doPing);
|
||||
pingStopBtn.addEventListener('click', stopPing);
|
||||
pingClearBtn.addEventListener('click', clearPingOutput);
|
||||
|
||||
const cardLogsElement = document.getElementById('card-logs-content');
|
||||
const refreshCardLogsBtn = document.getElementById('refresh-card-logs-btn');
|
||||
|
||||
function fetchCardLogs() {
|
||||
cardLogsElement.textContent = 'загрузка...';
|
||||
fetch('/api/card/card-logs')
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Не удалось загрузить логи считывателя');
|
||||
return response.text();
|
||||
})
|
||||
.then(data => {
|
||||
cardLogsElement.textContent = data || 'Лог-файл пуст.';
|
||||
})
|
||||
.catch(err => {
|
||||
cardLogsElement.textContent = `Ошибка при загрузке логов: ${err.message}`;
|
||||
cardLogsElement.style.color = '#ff6b6b';
|
||||
});
|
||||
}
|
||||
refreshCardLogsBtn.addEventListener('click', fetchCardLogs);
|
||||
|
||||
// --- ТАБЫ ---
|
||||
const tabMainBtn = document.getElementById('tab-main');
|
||||
const tabSettingsBtn = document.getElementById('tab-settings');
|
||||
const tabMonitoringBtn = document.getElementById('tab-monitoring');
|
||||
const mainTab = document.getElementById('main-tab');
|
||||
const settingsTab = document.getElementById('settings-tab');
|
||||
const monitoringTab = document.getElementById('monitoring-tab');
|
||||
|
||||
function showTab(tab) {
|
||||
mainTab.style.display = 'none';
|
||||
settingsTab.style.display = 'none';
|
||||
monitoringTab.style.display = 'none';
|
||||
tabMainBtn.classList.remove('active');
|
||||
tabSettingsBtn.classList.remove('active');
|
||||
tabMonitoringBtn.classList.remove('active');
|
||||
|
||||
if (tab === 'main') {
|
||||
mainTab.style.display = '';
|
||||
tabMainBtn.classList.add('active');
|
||||
} else if (tab === 'settings') {
|
||||
settingsTab.style.display = '';
|
||||
tabSettingsBtn.classList.add('active');
|
||||
initSettingsTab();
|
||||
} else if (tab === 'monitoring') {
|
||||
monitoringTab.style.display = '';
|
||||
tabMonitoringBtn.classList.add('active');
|
||||
initMonitoringTab();
|
||||
}
|
||||
}
|
||||
tabMainBtn.addEventListener('click', () => showTab('main'));
|
||||
tabSettingsBtn.addEventListener('click', () => showTab('settings'));
|
||||
tabMonitoringBtn.addEventListener('click', () => showTab('monitoring'));
|
||||
|
||||
// --- TOAST ---
|
||||
function showToast(message, type = 'success') {
|
||||
const container = document.getElementById('toast-container');
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast toast-' + type;
|
||||
toast.textContent = message;
|
||||
container.appendChild(toast);
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
setTimeout(() => container.removeChild(toast), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// --- CSRF TOKEN ---
|
||||
let csrfToken = null;
|
||||
async function fetchCsrfToken() {
|
||||
try {
|
||||
const res = await fetch('/moders/csrf-token', { credentials: 'include' });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
csrfToken = data.csrfToken;
|
||||
}
|
||||
} catch (e) { csrfToken = null; }
|
||||
}
|
||||
// Обёртка для fetch с CSRF и credentials
|
||||
async function apiFetch(url, options = {}) {
|
||||
if (!options.headers) options.headers = {};
|
||||
if (['POST','PUT','DELETE'].includes((options.method||'GET').toUpperCase())) {
|
||||
if (!csrfToken) await fetchCsrfToken();
|
||||
options.headers['X-CSRF-Token'] = csrfToken;
|
||||
}
|
||||
options.credentials = 'include';
|
||||
return fetch(url, options);
|
||||
}
|
||||
|
||||
// --- Модальное окно подтверждения ---
|
||||
const confirmModal = document.getElementById('confirm-modal');
|
||||
const confirmModalText = document.getElementById('confirm-modal-text');
|
||||
const confirmModalYes = document.getElementById('confirm-modal-yes');
|
||||
const confirmModalNo = document.getElementById('confirm-modal-no');
|
||||
|
||||
function showConfirm(text, onConfirm) {
|
||||
confirmModalText.textContent = text;
|
||||
confirmModal.style.display = 'flex';
|
||||
|
||||
const yesListener = () => {
|
||||
onConfirm();
|
||||
closeConfirm();
|
||||
};
|
||||
|
||||
const closeConfirm = () => {
|
||||
confirmModal.style.display = 'none';
|
||||
confirmModalYes.removeEventListener('click', yesListener);
|
||||
confirmModalNo.removeEventListener('click', closeConfirm);
|
||||
}
|
||||
|
||||
confirmModalYes.addEventListener('click', yesListener);
|
||||
confirmModalNo.addEventListener('click', closeConfirm);
|
||||
}
|
||||
|
||||
|
||||
// --- ВКЛАДКА МОНИТОРИНГА ---
|
||||
let monitoringInitialized = false;
|
||||
let allMonitoringEvents = []; // Кэш для всех событий
|
||||
|
||||
async function initMonitoringTab() {
|
||||
if (monitoringInitialized) return;
|
||||
monitoringInitialized = true;
|
||||
|
||||
const tableBody = document.querySelector('#monitoring-log-table tbody');
|
||||
const levelFilter = document.getElementById('log-level-filter');
|
||||
const sourceFilter = document.getElementById('log-source-filter');
|
||||
const refreshBtn = document.getElementById('refresh-monitoring-btn');
|
||||
|
||||
async function loadMonitoringLogs() {
|
||||
tableBody.innerHTML = '<tr><td colspan="4">Загрузка...</td></tr>';
|
||||
try {
|
||||
const res = await apiFetch('/api/monitoring/logs');
|
||||
if (!res.ok) throw new Error('Failed to fetch logs');
|
||||
|
||||
allMonitoringEvents = await res.json();
|
||||
renderTable();
|
||||
|
||||
} catch (e) {
|
||||
tableBody.innerHTML = '<tr><td colspan="4" style="color: red;">Ошибка загрузки логов.</td></tr>';
|
||||
console.error('Error loading monitoring logs:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
const level = levelFilter.value;
|
||||
const source = sourceFilter.value;
|
||||
|
||||
const filteredEvents = allMonitoringEvents.filter(event => {
|
||||
const levelMatch = (level === 'all') || (event.level === level);
|
||||
const sourceMatch = (source === 'all') || (event.source === source);
|
||||
return levelMatch && sourceMatch;
|
||||
});
|
||||
|
||||
tableBody.innerHTML = '';
|
||||
if (filteredEvents.length === 0) {
|
||||
tableBody.innerHTML = '<tr><td colspan="4">Нет событий для отображения.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
filteredEvents.forEach(event => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = `log-level-${event.level.toLowerCase()}`;
|
||||
tr.innerHTML = `
|
||||
<td>${new Date(event.timestamp).toLocaleString()}</td>
|
||||
<td><span class="log-level-label">${event.level}</span></td>
|
||||
<td>${event.source}</td>
|
||||
<td>${event.message}</td>
|
||||
`;
|
||||
tableBody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
levelFilter.addEventListener('change', renderTable);
|
||||
sourceFilter.addEventListener('change', renderTable);
|
||||
refreshBtn.addEventListener('click', loadMonitoringLogs);
|
||||
|
||||
loadMonitoringLogs();
|
||||
}
|
||||
|
||||
|
||||
// --- НАСТРОЙКИ: инициализация вкладки ---
|
||||
let settingsInitialized = false;
|
||||
|
||||
async function initSettingsTab() {
|
||||
if(settingsInitialized) return;
|
||||
settingsInitialized = true;
|
||||
|
||||
initServerParamsSection();
|
||||
initDbSection();
|
||||
initCorsSection();
|
||||
initModersSection();
|
||||
|
||||
settingsTab.addEventListener('click', (e) => {
|
||||
if (e.target.dataset.action === 'edit-pass') {
|
||||
const targetId = e.target.dataset.target;
|
||||
const input = document.getElementById(targetId);
|
||||
input.readOnly = false;
|
||||
input.value = '';
|
||||
input.placeholder = 'Введите новый пароль...';
|
||||
input.focus();
|
||||
e.target.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function initServerParamsSection() {
|
||||
// Загрузка текущих значений
|
||||
try {
|
||||
const results = await Promise.allSettled([
|
||||
apiFetch('/api/settings/admin-server'),
|
||||
apiFetch('/api/settings/backend-server')
|
||||
]);
|
||||
|
||||
const adminResult = results[0];
|
||||
if (adminResult.status === 'fulfilled') {
|
||||
const adminData = await adminResult.value.json();
|
||||
document.getElementById('admin-port').value = adminData.PORT || '';
|
||||
document.getElementById('admin-url').value = adminData.ADMIN_URL || '';
|
||||
} else {
|
||||
console.error('Failed to load admin-server settings:', adminResult.reason);
|
||||
}
|
||||
|
||||
const backendResult = results[1];
|
||||
if (backendResult.status === 'fulfilled') {
|
||||
const backendData = await backendResult.value.json();
|
||||
document.getElementById('backend-host').value = backendData.host || '';
|
||||
document.getElementById('backend-port').value = backendData.port || '';
|
||||
} else {
|
||||
console.error('Failed to load backend-server settings:', backendResult.reason);
|
||||
document.getElementById('server-params-status').textContent += ' Ошибка загрузки параметров Backend.';
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
document.getElementById('server-params-status').textContent = 'Критическая ошибка загрузки параметров';
|
||||
document.getElementById('server-params-status').style.color = 'red';
|
||||
}
|
||||
// Обработка сохранения
|
||||
document.getElementById('server-params-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const PORT = document.getElementById('admin-port').value;
|
||||
const ADMIN_URL = document.getElementById('admin-url').value;
|
||||
const host = document.getElementById('backend-host').value;
|
||||
const port = document.getElementById('backend-port').value;
|
||||
const statusEl = document.getElementById('server-params-status');
|
||||
statusEl.textContent = '';
|
||||
try {
|
||||
const [adminRes, backendRes] = await Promise.all([
|
||||
apiFetch('/api/settings/admin-server', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ PORT, ADMIN_URL })
|
||||
}),
|
||||
apiFetch('/api/settings/backend-server', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ host, port })
|
||||
})
|
||||
]);
|
||||
if (adminRes.ok && backendRes.ok) {
|
||||
statusEl.innerHTML = 'Параметры успешно сохранены.<br><strong class="warning-text">Требуется перезапуск обоих серверов для применения!</strong>';
|
||||
statusEl.style.color = 'green';
|
||||
showToast('Параметры успешно сохранены', 'success');
|
||||
} else {
|
||||
statusEl.textContent = 'Ошибка сохранения';
|
||||
statusEl.style.color = 'red';
|
||||
showToast('Ошибка сохранения', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
statusEl.textContent = 'Ошибка сохранения';
|
||||
statusEl.style.color = 'red';
|
||||
showToast('Ошибка сохранения', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initDbSection() {
|
||||
// --- БД ---
|
||||
// MS SQL
|
||||
async function loadMssql() {
|
||||
try {
|
||||
const res = await apiFetch('/api/settings/mssql');
|
||||
const data = await res.json();
|
||||
document.getElementById('mssql-user').value = data.DB_USER || '';
|
||||
|
||||
const passWrapper = document.getElementById('mssql-pass').closest('.password-wrapper');
|
||||
passWrapper.innerHTML = `
|
||||
<input type="password" id="mssql-pass" readonly>
|
||||
<button type="button" class="btn-small" data-action="edit-pass" data-target="mssql-pass">Изменить</button>
|
||||
`;
|
||||
if (data.DB_PASSWORD) {
|
||||
passWrapper.querySelector('input').placeholder = '********';
|
||||
}
|
||||
|
||||
document.getElementById('mssql-server').value = data.DB_SERVER || '';
|
||||
document.getElementById('mssql-db').value = data.DB_NAME || '';
|
||||
} catch (e) {
|
||||
document.getElementById('mssql-status').textContent = 'Ошибка загрузки';
|
||||
document.getElementById('mssql-status').style.color = 'red';
|
||||
}
|
||||
}
|
||||
loadMssql();
|
||||
document.getElementById('mssql-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const payload = {
|
||||
DB_USER: document.getElementById('mssql-user').value,
|
||||
DB_SERVER: document.getElementById('mssql-server').value,
|
||||
DB_NAME: document.getElementById('mssql-db').value
|
||||
};
|
||||
const passInput = document.getElementById('mssql-pass');
|
||||
if (!passInput.readOnly) {
|
||||
payload.DB_PASSWORD = passInput.value;
|
||||
}
|
||||
|
||||
const statusEl = document.getElementById('mssql-status');
|
||||
statusEl.textContent = '';
|
||||
try {
|
||||
const res = await apiFetch('/api/settings/mssql', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (res.ok) {
|
||||
statusEl.innerHTML = 'Сохранено.<br><strong class="warning-text">Требуется перезапуск Admin-server для применения!</strong>';
|
||||
statusEl.style.color = 'green';
|
||||
showToast('Сохранено', 'success');
|
||||
loadMssql(); // Перезагружаем секцию, чтобы сбросить поле пароля
|
||||
} else {
|
||||
statusEl.textContent = 'Ошибка сохранения';
|
||||
statusEl.style.color = 'red';
|
||||
showToast('Ошибка сохранения', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
statusEl.textContent = 'Ошибка сохранения';
|
||||
statusEl.style.color = 'red';
|
||||
showToast('Ошибка сохранения', 'error');
|
||||
}
|
||||
});
|
||||
// MySQL
|
||||
async function loadMysql() {
|
||||
try {
|
||||
const res = await apiFetch('/api/settings/mysql');
|
||||
const data = await res.json();
|
||||
document.getElementById('mysql-host').value = data.MYSQL_HOST || '';
|
||||
document.getElementById('mysql-port').value = data.MYSQL_PORT || '';
|
||||
document.getElementById('mysql-user').value = data.MYSQL_USER || '';
|
||||
|
||||
const passWrapper = document.getElementById('mysql-pass').closest('.password-wrapper');
|
||||
passWrapper.innerHTML = `
|
||||
<input type="password" id="mysql-pass" readonly>
|
||||
<button type="button" class="btn-small" data-action="edit-pass" data-target="mysql-pass">Изменить</button>
|
||||
`;
|
||||
if (data.MYSQL_PASSWORD) {
|
||||
passWrapper.querySelector('input').placeholder = '********';
|
||||
}
|
||||
|
||||
document.getElementById('mysql-db').value = data.MYSQL_DATABASE || '';
|
||||
} catch (e) {
|
||||
document.getElementById('mysql-status').textContent = 'Ошибка загрузки';
|
||||
document.getElementById('mysql-status').style.color = 'red';
|
||||
}
|
||||
}
|
||||
loadMysql();
|
||||
document.getElementById('mysql-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const payload = {
|
||||
MYSQL_HOST: document.getElementById('mysql-host').value,
|
||||
MYSQL_PORT: document.getElementById('mysql-port').value,
|
||||
MYSQL_USER: document.getElementById('mysql-user').value,
|
||||
MYSQL_DATABASE: document.getElementById('mysql-db').value
|
||||
};
|
||||
const passInput = document.getElementById('mysql-pass');
|
||||
if (!passInput.readOnly) {
|
||||
payload.MYSQL_PASSWORD = passInput.value;
|
||||
}
|
||||
|
||||
const statusEl = document.getElementById('mysql-status');
|
||||
statusEl.textContent = '';
|
||||
try {
|
||||
const res = await apiFetch('/api/settings/mysql', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (res.ok) {
|
||||
statusEl.innerHTML = 'Сохранено.<br><strong class="warning-text">Требуется перезапуск Admin-server для применения!</strong>';
|
||||
statusEl.style.color = 'green';
|
||||
showToast('Сохранено', 'success');
|
||||
loadMysql(); // Перезагружаем секцию, чтобы сбросить поле пароля
|
||||
} else {
|
||||
statusEl.textContent = 'Ошибка сохранения';
|
||||
statusEl.style.color = 'red';
|
||||
showToast('Ошибка сохранения', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
statusEl.textContent = 'Ошибка сохранения';
|
||||
statusEl.style.color = 'red';
|
||||
showToast('Ошибка сохранения', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initCorsSection() {
|
||||
// --- CORS ---
|
||||
let allowedOrigins = [];
|
||||
async function loadCors() {
|
||||
const list = document.getElementById('cors-list');
|
||||
list.innerHTML = '<li>Загрузка...</li>';
|
||||
try {
|
||||
const res = await apiFetch('/api/settings/cors');
|
||||
const data = await res.json();
|
||||
allowedOrigins = Array.isArray(data.ALLOWED_ORIGINS) ? data.ALLOWED_ORIGINS : [];
|
||||
renderCorsList();
|
||||
} catch (e) {
|
||||
list.innerHTML = '<li>Ошибка загрузки</li>';
|
||||
}
|
||||
}
|
||||
const corsInput = document.getElementById('cors-new-origin');
|
||||
const corsAddBtn = document.getElementById('cors-add-btn');
|
||||
corsInput.addEventListener('input', () => {
|
||||
const val = corsInput.value.trim();
|
||||
corsAddBtn.disabled = !val || allowedOrigins.includes(val);
|
||||
});
|
||||
function renderCorsList() {
|
||||
const list = document.getElementById('cors-list');
|
||||
list.innerHTML = '';
|
||||
if (!allowedOrigins.length) {
|
||||
list.innerHTML = '<li>Нет origin</li>';
|
||||
return;
|
||||
}
|
||||
allowedOrigins.forEach((origin, idx) => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = origin + ' ';
|
||||
const delBtn = document.createElement('button');
|
||||
delBtn.textContent = 'Удалить';
|
||||
delBtn.className = 'btn-small';
|
||||
delBtn.addEventListener('click', () => {
|
||||
allowedOrigins.splice(idx, 1);
|
||||
renderCorsList();
|
||||
corsAddBtn.disabled = !corsInput.value.trim() || allowedOrigins.includes(corsInput.value.trim());
|
||||
});
|
||||
li.appendChild(delBtn);
|
||||
list.appendChild(li);
|
||||
});
|
||||
corsAddBtn.disabled = !corsInput.value.trim() || allowedOrigins.includes(corsInput.value.trim());
|
||||
}
|
||||
loadCors();
|
||||
document.getElementById('cors-add-btn').addEventListener('click', () => {
|
||||
const input = document.getElementById('cors-new-origin');
|
||||
const val = input.value.trim();
|
||||
if (val && !allowedOrigins.includes(val)) {
|
||||
allowedOrigins.push(val);
|
||||
input.value = '';
|
||||
renderCorsList();
|
||||
corsAddBtn.disabled = true;
|
||||
}
|
||||
});
|
||||
document.getElementById('cors-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const statusEl = document.getElementById('cors-status');
|
||||
statusEl.textContent = '';
|
||||
try {
|
||||
const res = await apiFetch('/api/settings/cors', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ALLOWED_ORIGINS: allowedOrigins })
|
||||
});
|
||||
if (res.ok) {
|
||||
statusEl.innerHTML = 'Сохранено.<br><strong class="warning-text">Требуется перезапуск Admin-server для применения!</strong>';
|
||||
statusEl.style.color = 'green';
|
||||
showToast('Сохранено', 'success');
|
||||
} else {
|
||||
statusEl.textContent = 'Ошибка сохранения';
|
||||
statusEl.style.color = 'red';
|
||||
showToast('Ошибка сохранения', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
statusEl.textContent = 'Ошибка сохранения';
|
||||
statusEl.style.color = 'red';
|
||||
showToast('Ошибка сохранения', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initModersSection() {
|
||||
// --- МОДЕРАТОРЫ ---
|
||||
async function loadModers() {
|
||||
const tableBody = document.querySelector('#moders-table tbody');
|
||||
tableBody.innerHTML = '<tr><td colspan="3">Загрузка...</td></tr>';
|
||||
try {
|
||||
const res = await apiFetch('/api/moders');
|
||||
const moders = await res.json();
|
||||
if (!Array.isArray(moders) || !moders.length) {
|
||||
tableBody.innerHTML = '<tr><td colspan="3">Нет модераторов</td></tr>';
|
||||
return;
|
||||
}
|
||||
tableBody.innerHTML = '';
|
||||
for (const moder of moders) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset.id = moder.id;
|
||||
tr.innerHTML = `<td>${moder.id}</td><td>${moder.username}</td>` +
|
||||
`<td>` +
|
||||
`<button class="btn-small" data-action="reset" title="Сменить пароль">🔑</button> ` +
|
||||
`<button class="btn-small" data-action="delete" title="Удалить">🗑️</button>` +
|
||||
`</td>`;
|
||||
tableBody.appendChild(tr);
|
||||
}
|
||||
} catch (e) {
|
||||
tableBody.innerHTML = '<tr><td colspan="3">Ошибка загрузки</td></tr>';
|
||||
}
|
||||
}
|
||||
loadModers();
|
||||
// Добавление модератора
|
||||
document.getElementById('add-moder-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const username = document.getElementById('add-moder-username').value.trim();
|
||||
const password = document.getElementById('add-moder-password').value;
|
||||
const statusEl = document.getElementById('add-moder-status');
|
||||
statusEl.textContent = '';
|
||||
if (!username || !password) {
|
||||
statusEl.textContent = 'Заполните все поля';
|
||||
statusEl.style.color = 'red';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await apiFetch('/api/moders', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
if (res.ok) {
|
||||
statusEl.textContent = 'Модератор добавлен';
|
||||
statusEl.style.color = 'green';
|
||||
document.getElementById('add-moder-form').reset();
|
||||
loadModers();
|
||||
showToast('Модератор добавлен', 'success');
|
||||
} else {
|
||||
const data = await res.json();
|
||||
statusEl.textContent = data.message || 'Ошибка';
|
||||
statusEl.style.color = 'red';
|
||||
showToast(data.message || 'Ошибка', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
statusEl.textContent = 'Ошибка';
|
||||
statusEl.style.color = 'red';
|
||||
showToast('Ошибка', 'error');
|
||||
}
|
||||
});
|
||||
// Действия: удалить/сменить пароль
|
||||
document.getElementById('moders-table').addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('button');
|
||||
if (!btn) return;
|
||||
const id = btn.closest('tr').dataset.id;
|
||||
const action = btn.getAttribute('data-action');
|
||||
if (action === 'delete') {
|
||||
showConfirm('Удалить модератора?', async () => {
|
||||
try {
|
||||
const res = await apiFetch(`/api/moders/${id}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
loadModers();
|
||||
showToast('Модератор удален', 'success');
|
||||
} else {
|
||||
showToast('Ошибка удаления', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('Ошибка удаления', 'error');
|
||||
}
|
||||
});
|
||||
} else if (action === 'reset') {
|
||||
const row = btn.closest('tr');
|
||||
const actionsCell = row.cells[2];
|
||||
|
||||
// Если мы уже в режиме редактирования, ничего не делаем
|
||||
if (actionsCell.querySelector('input')) return;
|
||||
|
||||
// Сохраняем оригинальные кнопки
|
||||
const originalButtons = actionsCell.innerHTML;
|
||||
|
||||
// Создаем поле ввода и кнопки
|
||||
actionsCell.innerHTML = `
|
||||
<input type="password" placeholder="Новый пароль" class="moder-pass-input">
|
||||
<button class="btn-small" data-action="save-pass" title="Сохранить">✅</button>
|
||||
<button class="btn-small" data-action="cancel-pass" title="Отмена">❌</button>
|
||||
`;
|
||||
|
||||
const input = actionsCell.querySelector('input');
|
||||
input.focus();
|
||||
|
||||
const saveBtn = actionsCell.querySelector('[data-action="save-pass"]');
|
||||
const cancelBtn = actionsCell.querySelector('[data-action="cancel-pass"]');
|
||||
|
||||
const handleSave = async () => {
|
||||
const newPass = input.value;
|
||||
if (!newPass || newPass.length < 4) {
|
||||
showToast('Пароль должен быть не менее 4 символов', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await apiFetch(`/api/moders/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password: newPass })
|
||||
});
|
||||
if (res.ok) {
|
||||
showToast('Пароль обновлён', 'success');
|
||||
actionsCell.innerHTML = originalButtons; // Возвращаем кнопки
|
||||
} else {
|
||||
showToast('Ошибка смены пароля', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('Ошибка смены пароля', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
actionsCell.innerHTML = originalButtons;
|
||||
};
|
||||
|
||||
saveBtn.addEventListener('click', handleSave);
|
||||
cancelBtn.addEventListener('click', handleCancel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetchStatus();
|
||||
fetchConfig();
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Модератор: наполнение вкладок</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link href="/libs/quill/quill.snow.css" rel="stylesheet">
|
||||
<script src="/libs/quill/quill.js"></script>
|
||||
<script src="/libs/dompurify/purify.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="notify"></div>
|
||||
<div class="container">
|
||||
<h1>Панель модератора</h1>
|
||||
<div id="login-section">
|
||||
<h2>Вход для модератора</h2>
|
||||
<form id="moder-login-form">
|
||||
<div class="form-group">
|
||||
<label for="username">Логин:</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Пароль:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit">Войти</button>
|
||||
<p id="login-error" style="color: red;"></p>
|
||||
</form>
|
||||
</div>
|
||||
<div id="editor-section" style="display:none;">
|
||||
<button id="logout-btn" style="float:right;">Выйти из системы</button>
|
||||
<h2>Редактирование вкладок</h2>
|
||||
<div class="tab-controls">
|
||||
<button class="tab-btn active" data-tab="info">Общая информация</button>
|
||||
<button class="tab-btn" data-tab="union">Профсоюз</button>
|
||||
</div>
|
||||
<div style="margin: 18px 0;">
|
||||
<button id="add-block-btn">Добавить блок</button>
|
||||
</div>
|
||||
<div id="blocks-list"></div>
|
||||
</div>
|
||||
<div id="edit-modal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<span class="close" id="edit-modal-close">×</span>
|
||||
<h3 id="edit-modal-title">Блок</h3>
|
||||
<form id="edit-block-form">
|
||||
<input type="hidden" id="edit-block-id">
|
||||
<div class="form-group">
|
||||
<label for="edit-quill-editor">Холст:</label>
|
||||
<div id="edit-quill-editor" style="height: 250px;"></div>
|
||||
</div>
|
||||
<div id="edit-preview-label" style="margin-top:10px;font-weight:bold;">Предпросмотр:</div>
|
||||
<div id="edit-block-preview" class="block-text" style="min-height:40px;margin-bottom:10px;"></div>
|
||||
<button type="submit">Сохранить</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="confirm-modal" class="modal" style="display:none; z-index:2000;">
|
||||
<div class="modal-content" style="max-width:400px; text-align:center;">
|
||||
<p style="margin-bottom:20px; ">Вы уверены, что хотите закрыть окно?<br>Несохранённые изменения будут потеряны!</p>
|
||||
<button id="confirm-close-yes" style="background:#e74c3c; margin-right:12px;">Да, закрыть</button>
|
||||
<button id="confirm-close-no">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="editor.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,209 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const loginSection = document.getElementById('login-section');
|
||||
const editorSection = document.getElementById('editor-section');
|
||||
const loginForm = document.getElementById('moder-login-form');
|
||||
const loginError = document.getElementById('login-error');
|
||||
const tabBtns = document.querySelectorAll('.tab-btn');
|
||||
const addBlockBtn = document.getElementById('add-block-btn');
|
||||
const blocksList = document.getElementById('blocks-list');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
loginError.textContent = '';
|
||||
const username = loginForm.username.value.trim();
|
||||
const password = loginForm.password.value;
|
||||
try {
|
||||
const res = await fetch('/moders/moder-login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
if (res.ok) {
|
||||
loginSection.style.display = 'none';
|
||||
editorSection.style.display = '';
|
||||
loadBlocks(currentTab);
|
||||
} else {
|
||||
const data = await res.json();
|
||||
loginError.textContent = data.message || 'Ошибка входа';
|
||||
}
|
||||
} catch (err) {
|
||||
loginError.textContent = 'Ошибка сервера';
|
||||
}
|
||||
});
|
||||
if (logoutBtn) {
|
||||
logoutBtn.onclick = async () => {
|
||||
await fetch('/moders/moder-logout');
|
||||
editorSection.style.display = 'none';
|
||||
loginSection.style.display = '';
|
||||
showNotify('Вы вышли из системы', 'success');
|
||||
};
|
||||
}
|
||||
tabBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
tabBtns.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
currentTab = btn.dataset.tab;
|
||||
loadBlocks(currentTab);
|
||||
});
|
||||
});
|
||||
function loadBlocks(tab) {
|
||||
fetch(`/moders/content/${tab}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
blocksList.innerHTML = '';
|
||||
if (!Array.isArray(data) || !data.length) {
|
||||
blocksList.innerHTML = '<p>Нет блоков</p>';
|
||||
return;
|
||||
}
|
||||
data.forEach(block => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'content-block';
|
||||
div.innerHTML = `<div class="block-preview" style="margin-bottom:10px;">${DOMPurify.sanitize(block.content)}</div>`;
|
||||
const editBtn = document.createElement('button');
|
||||
editBtn.textContent = 'Редактировать';
|
||||
editBtn.onclick = () => openEditModal(block);
|
||||
const delBtn = document.createElement('button');
|
||||
delBtn.textContent = 'Удалить';
|
||||
delBtn.onclick = async () => {
|
||||
if (confirm('Удалить блок?')) {
|
||||
await fetch(`/moders/content/${tab}/${block.id}`, { method: 'DELETE' });
|
||||
showNotify('Блок удалён', 'success');
|
||||
loadBlocks(tab);
|
||||
}
|
||||
};
|
||||
div.appendChild(editBtn);
|
||||
div.appendChild(delBtn);
|
||||
blocksList.appendChild(div);
|
||||
});
|
||||
});
|
||||
}
|
||||
const editModal = document.getElementById('edit-modal');
|
||||
const editModalClose = document.getElementById('edit-modal-close');
|
||||
const editBlockForm = document.getElementById('edit-block-form');
|
||||
const editBlockId = document.getElementById('edit-block-id');
|
||||
const editModalTitle = document.getElementById('edit-modal-title');
|
||||
let editQuill;
|
||||
let currentTab = 'info';
|
||||
let editingBlockId = null;
|
||||
let originalContent = '';
|
||||
function showNotify(msg, type = 'success') {
|
||||
const notify = document.getElementById('notify');
|
||||
notify.textContent = msg;
|
||||
notify.className = 'show ' + type;
|
||||
setTimeout(() => { notify.className = ''; }, 2200);
|
||||
}
|
||||
|
||||
if (typeof Quill === 'undefined' || typeof DOMPurify === 'undefined') {
|
||||
console.error('Required libraries not loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
// Инициализируйте Quill
|
||||
if (!initQuill()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
function initQuill() {
|
||||
if (typeof Quill === 'undefined') {
|
||||
console.error('Quill not loaded');
|
||||
return false;
|
||||
}
|
||||
editQuill = new Quill('#edit-quill-editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{ 'align': [] }],
|
||||
[{ 'color': [] }, { 'background': [] }],
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['blockquote', 'code-block'],
|
||||
['link', 'image', 'video'],
|
||||
['clean']
|
||||
]
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
function openEditModal(block) {
|
||||
editingBlockId = block ? block.id : null;
|
||||
editBlockId.value = block ? block.id : '';
|
||||
editModalTitle.textContent = block ? 'Редактировать блок' : 'Добавить блок';
|
||||
editQuill.root.innerHTML = block ? block.content : '';
|
||||
document.getElementById('edit-block-preview').innerHTML = block ? DOMPurify.sanitize(block.content) : '';
|
||||
originalContent = block ? block.content : '';
|
||||
editModal.style.display = 'block';
|
||||
}
|
||||
editQuill.on('text-change', () => {
|
||||
document.getElementById('edit-block-preview').innerHTML = DOMPurify.sanitize(editQuill.root.innerHTML);
|
||||
});
|
||||
function tryCloseEditModal() {
|
||||
const currentContent = editQuill.root.innerHTML;
|
||||
if (currentContent !== originalContent) {
|
||||
document.getElementById('confirm-modal').style.display = 'flex';
|
||||
const yesBtn = document.getElementById('confirm-close-yes');
|
||||
const noBtn = document.getElementById('confirm-close-no');
|
||||
yesBtn.onclick = function() {
|
||||
document.getElementById('confirm-modal').style.display = 'none';
|
||||
editModal.style.display = 'none';
|
||||
};
|
||||
noBtn.onclick = function() {
|
||||
document.getElementById('confirm-modal').style.display = 'none';
|
||||
};
|
||||
return;
|
||||
}
|
||||
editModal.style.display = 'none';
|
||||
}
|
||||
editModalClose.onclick = tryCloseEditModal;
|
||||
window.onclick = (event) => {
|
||||
if (event.target === editModal) tryCloseEditModal();
|
||||
};
|
||||
addBlockBtn.onclick = () => openEditModal(null);
|
||||
editBlockForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const content = DOMPurify.sanitize(editQuill.root.innerHTML.trim());
|
||||
if (!content || content === '<p><br></p>') return;
|
||||
if (editingBlockId) {
|
||||
await fetch(`/moders/content/${currentTab}/${editingBlockId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type: 'text', content })
|
||||
});
|
||||
showNotify('Блок обновлён', 'success');
|
||||
} else {
|
||||
await fetch(`/moders/content/${currentTab}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type: 'text', content })
|
||||
});
|
||||
showNotify('Блок добавлен', 'success');
|
||||
}
|
||||
editModal.style.display = 'none';
|
||||
loadBlocks(currentTab);
|
||||
});
|
||||
fetch('/moders/check-session', { credentials: 'include' }).then(res => {
|
||||
if (res.ok) {
|
||||
loginSection.style.display = 'none';
|
||||
editorSection.style.display = '';
|
||||
loadBlocks(currentTab);
|
||||
} else {
|
||||
loginSection.style.display = '';
|
||||
editorSection.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
let csrfToken = '';
|
||||
fetch('/moders/csrf-token').then(res => res.json()).then(data => {
|
||||
csrfToken = data.csrfToken;
|
||||
});
|
||||
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = function(url, options = {}) {
|
||||
if (options && options.method && ['POST','PUT','DELETE'].includes(options.method.toUpperCase())) {
|
||||
options.headers = options.headers || {};
|
||||
options.headers['x-csrf-token'] = csrfToken;
|
||||
}
|
||||
return originalFetch(url, options);
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,201 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Панель администратора</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="toast-container"></div>
|
||||
<div class="container">
|
||||
<h1>Панель администратора</h1>
|
||||
<div class="tabs">
|
||||
<button id="tab-main" class="tab-btn active">Общее</button>
|
||||
<button id="tab-settings" class="tab-btn">Настройки</button>
|
||||
<button id="tab-monitoring" class="tab-btn">Мониторинг</button>
|
||||
</div>
|
||||
<div id="main-tab">
|
||||
<div id="status">
|
||||
<p>Статус сервера: <span id="server-status">загрузка...</span></p>
|
||||
<button id="refresh-status-btn" class="btn-small">Обновить</button>
|
||||
</div>
|
||||
|
||||
<div id="db-status-container">
|
||||
<div>
|
||||
<p>Админ -> БД: <span id="admin-db-status">неизвестно</span></p>
|
||||
<small id="admin-db-message"></small>
|
||||
</div>
|
||||
<div>
|
||||
<p>Программа -> БД: <span id="backend-db-status">неизвестно</span></p>
|
||||
<small id="backend-db-message"></small>
|
||||
</div>
|
||||
<button id="check-db-btn" class="btn-small">Проверить</button>
|
||||
</div>
|
||||
|
||||
<div class="network-diag-container">
|
||||
<h2>Сетевая диагностика (Ping)</h2>
|
||||
<div class="ping-controls">
|
||||
<input type="text" id="ping-ip" placeholder="Введите IP-адрес или хост">
|
||||
<button id="ping-btn" class="btn-small">Пинг</button>
|
||||
<button id="ping-stop-btn" class="btn-small" disabled>Стоп</button>
|
||||
<button id="ping-clear-btn" class="btn-small">Очистить</button>
|
||||
</div>
|
||||
<pre id="ping-output"></pre>
|
||||
</div>
|
||||
|
||||
<div class="logs-container">
|
||||
<h2>Логи приложения</h2>
|
||||
<div class="log-controls">
|
||||
<button id="refresh-logs-btn" class="btn-small">Обновить логи</button>
|
||||
<button id="clear-logs-btn" class="btn-small">Очистить</button>
|
||||
</div>
|
||||
<pre id="logs-content">Нажмите "Обновить логи" для просмотра</pre>
|
||||
</div>
|
||||
|
||||
<div class="logs-container">
|
||||
<h2>Логи считывателя</h2>
|
||||
<div class="log-controls">
|
||||
<button id="refresh-card-logs-btn" class="btn-small">Обновить логи считывателя</button>
|
||||
</div>
|
||||
<pre id="ping-output">
|
||||
<pre id="card-logs-content">Нажмите "Обновить логи считывателя" для просмотра</pre>
|
||||
</pre></pre>
|
||||
</div>
|
||||
|
||||
<div class="config-container">
|
||||
<h2>Конфигурация</h2>
|
||||
<form id="config-form">
|
||||
<div class="form-group">
|
||||
<label for="payslipsPath">Путь к расчетным листкам:</label>
|
||||
<div class="input-with-button">
|
||||
<input type="text" id="payslipsPath" name="payslipsPath" required>
|
||||
<button type="button" id="browse-btn">Обзор...</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit">Сохранить</button>
|
||||
</form>
|
||||
<p id="config-status"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings-tab" style="display:none">
|
||||
<h2>Настройки</h2>
|
||||
<div id="server-params-section"><h3>Параметры серверов</h3>
|
||||
<form id="server-params-form" novalidate>
|
||||
<fieldset><legend>Admin-server</legend>
|
||||
<label>PORT: <input type="number" id="admin-port" min="1" max="65535" required></label><br>
|
||||
<label>ADMIN_URL: <input type="text" id="admin-url" pattern="https?://.+" required></label>
|
||||
</fieldset>
|
||||
<fieldset><legend>Backend</legend>
|
||||
<label>host: <input type="text" id="backend-host" required></label><br>
|
||||
<label>port: <input type="number" id="backend-port" min="1" max="65535" required></label>
|
||||
</fieldset>
|
||||
<button type="submit">Сохранить</button>
|
||||
<span id="server-params-status" class="form-error"></span>
|
||||
</form>
|
||||
</div>
|
||||
<div id="db-section"><h3>Подключения к БД</h3>
|
||||
<form id="mssql-form" novalidate><fieldset><legend>MS SQL</legend>
|
||||
<label>DB_USER: <input type="text" id="mssql-user" required></label><br>
|
||||
<label>DB_PASSWORD:
|
||||
<div class="password-wrapper">
|
||||
<input type="password" id="mssql-pass" readonly>
|
||||
<button type="button" class="btn-small" data-action="edit-pass" data-target="mssql-pass">Изменить</button>
|
||||
</div>
|
||||
</label><br>
|
||||
<label>DB_SERVER: <input type="text" id="mssql-server" required></label><br>
|
||||
<label>DB_NAME: <input type="text" id="mssql-db" required></label><br>
|
||||
<button type="submit">Сохранить</button>
|
||||
<span id="mssql-status" class="form-error"></span>
|
||||
</fieldset></form>
|
||||
<form id="mysql-form" novalidate><fieldset><legend>MySQL (модераторы)</legend>
|
||||
<label>MYSQL_HOST: <input type="text" id="mysql-host" required></label><br>
|
||||
<label>MYSQL_PORT: <input type="number" id="mysql-port" min="1" max="65535" required></label><br>
|
||||
<label>MYSQL_USER: <input type="text" id="mysql-user" required></label><br>
|
||||
<label>MYSQL_PASSWORD:
|
||||
<div class="password-wrapper">
|
||||
<input type="password" id="mysql-pass" readonly>
|
||||
<button type="button" class="btn-small" data-action="edit-pass" data-target="mysql-pass">Изменить</button>
|
||||
</div>
|
||||
</label><br>
|
||||
<label>MYSQL_DATABASE: <input type="text" id="mysql-db" required></label><br>
|
||||
<button type="submit">Сохранить</button>
|
||||
<span id="mysql-status" class="form-error"></span>
|
||||
</fieldset></form>
|
||||
</div>
|
||||
<div id="cors-section"><h3>CORS (ALLOWED_ORIGINS)</h3>
|
||||
<form id="cors-form">
|
||||
<ul id="cors-list"></ul>
|
||||
<input type="text" id="cors-new-origin" placeholder="Добавить origin...">
|
||||
<button type="button" id="cors-add-btn" disabled>Добавить</button>
|
||||
<button type="submit">Сохранить</button>
|
||||
<span id="cors-status" class="form-error"></span>
|
||||
</form>
|
||||
</div>
|
||||
<div id="moders-section"><h3>Модераторы</h3>
|
||||
<form id="add-moder-form" novalidate>
|
||||
<input type="text" id="add-moder-username" placeholder="Логин" required>
|
||||
<input type="password" id="add-moder-password" placeholder="Пароль (мин. 4 символа)" minlength="4" required>
|
||||
<button type="submit">Добавить</button>
|
||||
<span id="add-moder-status" class="form-error"></span>
|
||||
</form>
|
||||
<table id="moders-table" class="moders-table"><thead><tr><th>ID</th><th>Логин</th><th>Действия</th></tr></thead><tbody></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="monitoring-tab" style="display:none;">
|
||||
<h2>Мониторинг системы</h2>
|
||||
<div id="monitoring-status-container">
|
||||
<!-- Статусы будут здесь -->
|
||||
</div>
|
||||
|
||||
<h3>Проблемные события (Warn/Error)</h3>
|
||||
<div class="monitoring-filters">
|
||||
<label>Уровень:
|
||||
<select id="log-level-filter">
|
||||
<option value="all">Все</option>
|
||||
<option value="ERROR">Error</option>
|
||||
<option value="WARN">Warn</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Источник:
|
||||
<select id="log-source-filter">
|
||||
<option value="all">Все</option>
|
||||
<option value="Admin">Admin</option>
|
||||
<option value="Backend">Backend</option>
|
||||
<option value="CardReader">CardReader</option>
|
||||
</select>
|
||||
</label>
|
||||
<button id="refresh-monitoring-btn" class="btn-small">Обновить</button>
|
||||
</div>
|
||||
<table id="monitoring-log-table" class="moders-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 20%;">Время</th>
|
||||
<th style="width: 10%;">Уровень</th>
|
||||
<th style="width: 15%;">Источник</th>
|
||||
<th>Сообщение</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Данные будут здесь -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для подтверждений -->
|
||||
<div id="confirm-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<p id="confirm-modal-text">Вы уверены?</p>
|
||||
<div class="modal-buttons">
|
||||
<button id="confirm-modal-yes" class="btn-danger">Да</button>
|
||||
<button id="confirm-modal-no" class="btn-secondary">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Вход в панель администратора</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Вход</h1>
|
||||
<form id="login-form">
|
||||
<div class="form-group">
|
||||
<label for="password">Пароль:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit">Войти</button>
|
||||
</form>
|
||||
<p id="error-message" style="color: red;"></p>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('login-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const password = document.getElementById('password').value;
|
||||
const response = await fetch('/api/auth', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
document.getElementById('error-message').textContent = 'Неверный пароль';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,557 @@
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #f4f7f9;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 80%;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #0056b3;
|
||||
}
|
||||
|
||||
#status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
#refresh-status-btn {
|
||||
padding: 5px 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#server-status {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.log-controls {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ql-align-center { text-align: center; }
|
||||
.ql-align-right { text-align: right; }
|
||||
.ql-align-justify { text-align: justify; }
|
||||
|
||||
#logs-content {
|
||||
background-color: #2d2d2d;
|
||||
color: #f1f1f1;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
white-space: pre-wrap; /* Перенос строк */
|
||||
word-wrap: break-word; /* Перенос длинных строк */
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.config-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.input-with-button {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.input-with-button input {
|
||||
flex-grow: 1; /* Поле ввода займет все доступное место */
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #0056b3;
|
||||
color: white;
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #004494;
|
||||
}
|
||||
|
||||
#db-status-container {
|
||||
display: flex;
|
||||
align-items: flex-start; /* Выравнивание по верху */
|
||||
gap: 20px;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#db-status-container p {
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
#db-status-container small {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#refresh-status-btn, .btn-small {
|
||||
padding: 5px 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.network-diag-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.ping-controls {
|
||||
display: inline-flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.ping-controls input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#ping-output {
|
||||
background-color: #2d2d2d;
|
||||
color: #f1f1f1;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
/* --- Карточки блоков --- */
|
||||
.content-block {
|
||||
background: #f9f9fb;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
|
||||
padding: 18px 18px 12px 18px;
|
||||
margin-bottom: 18px;
|
||||
position: relative;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
.content-block:hover {
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.13);
|
||||
}
|
||||
.block-text, .block-image, .block-video {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.block-image img, .block-video video {
|
||||
max-width: 100%;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
/* --- Кнопки --- */
|
||||
button, .btn-small, .tab-btn {
|
||||
background: #0056b3;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 8px 16px;
|
||||
margin-right: 8px;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, box-shadow 0.15s;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
|
||||
}
|
||||
button:hover, .btn-small:hover, .tab-btn:hover {
|
||||
background: #003e80;
|
||||
}
|
||||
button:active, .btn-small:active, .tab-btn:active {
|
||||
background: #002b5c;
|
||||
}
|
||||
.tab-btn.active {
|
||||
background: #0074e4;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* --- Модальное окно --- */
|
||||
.modal {
|
||||
display: none; /* Скрыто по умолчанию */
|
||||
position: fixed; /* Остается на месте при скролле */
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto; /* Включает скролл, если нужно */
|
||||
background-color: rgba(0,0,0,0.5); /* Полупрозрачный фон */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 80%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.monitoring-filters {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.monitoring-filters label {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.log-level-label {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
tr.log-level-error {
|
||||
background-color: #ffdddd;
|
||||
}
|
||||
tr.log-level-error .log-level-label {
|
||||
background-color: #d63031;
|
||||
}
|
||||
|
||||
tr.log-level-warn {
|
||||
background-color: #fff3cd;
|
||||
}
|
||||
tr.log-level-warn .log-level-label {
|
||||
background-color: #fdcb6e;
|
||||
}
|
||||
|
||||
|
||||
.modal-buttons {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.modal-buttons button {
|
||||
margin: 0 10px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
top: 12px;
|
||||
font-size: 2rem;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.close:hover { color: #0056b3; }
|
||||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||
@keyframes slideDown { from { transform: translateY(-30px); opacity: 0; } to { transform: none; opacity: 1; } }
|
||||
|
||||
/* --- Уведомления --- */
|
||||
#notify {
|
||||
position: fixed;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
z-index: 2000;
|
||||
min-width: 220px;
|
||||
background: #fff;
|
||||
color: #222;
|
||||
border-radius: 7px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.13);
|
||||
padding: 16px 22px;
|
||||
font-size: 16px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
#notify.show {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
#notify.error {
|
||||
background: #ffeaea;
|
||||
color: #b30000;
|
||||
border: 1px solid #ffb3b3;
|
||||
}
|
||||
#notify.success {
|
||||
background: #eaffea;
|
||||
color: #1a7f1a;
|
||||
border: 1px solid #b3ffb3;
|
||||
}
|
||||
|
||||
/* --- Формы --- */
|
||||
input[type="text"], input[type="password"], textarea, select {
|
||||
border: 1.5px solid #b3c6e0;
|
||||
border-radius: 5px;
|
||||
padding: 8px 10px;
|
||||
font-size: 15px;
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
transition: border 0.15s;
|
||||
}
|
||||
input[type="text"]:focus, input[type="password"]:focus, textarea:focus, select:focus {
|
||||
border: 1.5px solid #0056b3;
|
||||
outline: none;
|
||||
background: #f4f8ff;
|
||||
}
|
||||
|
||||
/* --- Logout --- */
|
||||
#logout-btn {
|
||||
background: #e74c3c;
|
||||
float: right;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 15px;
|
||||
padding: 7px 16px;
|
||||
}
|
||||
#logout-btn:hover { background: #c0392b; }
|
||||
|
||||
/* Кнопка "Добавить блок" */
|
||||
#add-block-btn {
|
||||
background: #0074e4;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 8px 18px;
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
#add-block-btn:hover { background: #005bb5; }
|
||||
|
||||
/* Кнопка "Редактировать" */
|
||||
.content-block button:nth-of-type(1) {
|
||||
background: #ff9800;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 6px 14px;
|
||||
margin-right: 8px;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.content-block button:nth-of-type(1):hover { background: #e65100; }
|
||||
|
||||
/* Кнопка "Удалить" */
|
||||
.content-block button:nth-of-type(2) {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 6px 14px;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.content-block button:nth-of-type(2):hover { background: #b71c1c; }
|
||||
|
||||
/* Кнопка "Сохранить" в модалке */
|
||||
#edit-block-form button[type="submit"] {
|
||||
background: #43a047;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 8px 18px;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
#edit-block-form button[type="submit"]:hover { background: #2e7031; }
|
||||
|
||||
/* --- Адаптивность --- */
|
||||
@media (max-width: 600px) {
|
||||
.container { padding: 8px; }
|
||||
.modal-content { min-width: 90vw; padding: 18px 8px; }
|
||||
#notify { right: 8px; top: 8px; min-width: 120px; font-size: 14px; }
|
||||
}
|
||||
|
||||
#edit-block-preview {
|
||||
max-width: 100%;
|
||||
max-height: 45vh; /* Ограничение по высоте предпросмотра */
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#confirm-modal .modal-content {
|
||||
max-width: 400px;
|
||||
padding: 10px 28px 24px 28px;
|
||||
text-align: center;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.22);
|
||||
}
|
||||
#confirm-modal h3 {
|
||||
font-size: 1.15rem;
|
||||
color: #222;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
#confirm-close-yes {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 8px 18px;
|
||||
font-size: 16px;
|
||||
margin-right: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
#confirm-close-yes:hover { background: #b71c1c; }
|
||||
#confirm-close-no {
|
||||
background: #f4f7f9;
|
||||
color: #222;
|
||||
border: 1.5px solid #b3c6e0;
|
||||
border-radius: 5px;
|
||||
padding: 8px 18px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
#confirm-close-no:hover { background: #e0e7ef; }
|
||||
|
||||
#toast-container {
|
||||
position: fixed;
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.toast {
|
||||
visibility: hidden;
|
||||
min-width: 250px;
|
||||
max-width: 350px;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
padding: 14px 22px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
font-size: 16px;
|
||||
opacity: 0.97;
|
||||
animation: fadein 0.3s, fadeout 0.3s 2.7s;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.toast-success { background: #2ecc40; color: #fff; }
|
||||
.toast-error { background: #ff4136; color: #fff; }
|
||||
@keyframes fadein { from { opacity: 0; transform: translateY(-20px);} to { opacity: 0.97; transform: translateY(0);} }
|
||||
@keyframes fadeout { from { opacity: 0.97; } to { opacity: 0; } }
|
||||
|
||||
/* --- Настройки: секции и формы --- */
|
||||
#settings-tab > div {
|
||||
margin-bottom: 32px;
|
||||
padding: 18px 20px 16px 20px;
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
|
||||
border: 1px solid #e3e8ee;
|
||||
}
|
||||
#settings-tab h3 { margin-top: 0; }
|
||||
fieldset {
|
||||
border: 1px solid #bfc9d8;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px 16px 10px 16px;
|
||||
background: #fff;
|
||||
}
|
||||
legend { color: #0056b3; font-weight: 500; }
|
||||
input[type="text"], input[type="password"], input[type="number"] {
|
||||
padding: 7px 10px;
|
||||
border: 1px solid #bfc9d8;
|
||||
border-radius: 4px;
|
||||
margin: 4px 0 10px 0;
|
||||
width: 220px;
|
||||
font-size: 15px;
|
||||
background: #fafdff;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
input:focus { border: 1.5px solid #0056b3; outline: none; background: #fff; }
|
||||
input:invalid { border: 1.5px solid #ff4136; background: #fff6f6; }
|
||||
.form-error { color: #ff4136; font-size: 14px; margin: 2px 0 8px 0; }
|
||||
.warning-text {
|
||||
color: #ff8c00; /* DarkOrange */
|
||||
}
|
||||
.btn-small {
|
||||
background: #e3e8ee;
|
||||
color: #0056b3;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 5px 12px;
|
||||
font-size: 14px;
|
||||
margin-left: 2px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
.btn-small:hover { background: #0056b3; color: #fff; }
|
||||
button[type="submit"] {
|
||||
background: #2ecc40;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 18px;
|
||||
font-size: 15px;
|
||||
margin-top: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
button[type="submit"]:hover { background: #27ae38; }
|
||||
#cors-list li { margin-bottom: 6px; }
|
||||
.moders-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
.moders-table th, .moders-table td {
|
||||
border: 1px solid #e3e8ee;
|
||||
padding: 7px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
.moders-table th {
|
||||
background: #f4f7fa;
|
||||
color: #0056b3;
|
||||
font-weight: 500;
|
||||
}
|
||||
.moders-table tr:nth-child(even) { background: #fafdff; }
|
||||
.moders-table tr:hover { background: #eaf3ff; }
|
||||
|
||||
.moder-pass-input {
|
||||
width: 100px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.password-wrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
Reference in New Issue
Block a user