сборка для Ильи

This commit is contained in:
Антон committed 2026-09-20 19:47:02 +03:00
commit 59ce64be5c
18 files changed
+2400

No files matched your search

+289
View File
@@ -0,0 +1,289 @@
/* admin.css - Updated to match index.html aesthetics */
:root {
--primary-gradient: linear-gradient(135deg, #1a2980 0%, #26d0ce 100%);
--glass-bg: rgba(255, 255, 255, 0.95);
--text-color: #333;
--heading-color: #2c3e50;
--success-color: #2ecc71;
--danger-color: #e74c3c;
--info-color: #3498db;
--radius-lg: 20px;
--radius-md: 12px;
--shadow-heavy: 0 15px 35px rgba(0, 0, 0, 0.2);
--shadow-light: 0 4px 6px rgba(0, 0, 0, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Outfit', 'Segoe UI', sans-serif;
}
body {
background: var(--primary-gradient);
background-attachment: fixed;
min-height: 100vh;
padding: 30px 20px;
color: var(--text-color);
}
.admin-header {
max-width: 1200px;
margin: 0 auto 40px auto;
display: flex;
justify-content: space-between;
align-items: center;
color: white;
}
.admin-header h2 {
font-size: 2.2rem;
font-weight: 700;
letter-spacing: -0.5px;
text-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
.admin-container {
max-width: 1200px;
margin: 0 auto;
}
/* Management Cards - Glassmorphism */
.management-card {
background-color: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-heavy);
padding: 40px;
margin-bottom: 40px;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.card-title {
color: var(--heading-color);
margin-bottom: 20px;
font-size: 1.4rem;
display: flex;
align-items: center;
gap: 10px;
}
/* Stats Cards */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
padding: 25px;
border-radius: var(--radius-md);
box-shadow: var(--shadow-light);
text-align: center;
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-card h3 {
font-size: 0.9rem;
color: #7f8c8d;
text-transform: uppercase;
letter-spacing: 1px;
}
.stat-card .value {
font-size: 2.5rem;
font-weight: 700;
margin-top: 10px;
}
/* Form Styles */
.inline-form {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)) auto;
gap: 15px;
align-items: flex-end;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-size: 0.9rem;
font-weight: 600;
color: #555;
}
input,
select {
padding: 12px 15px;
border: 2px solid #eee;
border-radius: 10px;
font-size: 1rem;
transition: all 0.3s ease;
background-color: white;
width: 100%;
}
select {
cursor: pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2395a5a6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 15px center;
background-size: 18px;
padding-right: 45px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
input:focus,
select:focus {
outline: none;
border-color: var(--info-color);
box-shadow: 0 0 0 4px rgba(52, 152, 219, 0.1);
}
/* Table Styles - Premium Look */
.table-container {
overflow-x: auto;
border-radius: var(--radius-md);
border: 1px solid #eee;
}
table {
width: 100%;
border-collapse: collapse;
background: white;
}
th {
background-color: #f8f9fa;
padding: 15px;
text-align: left;
font-weight: 600;
color: #555;
border-bottom: 2px solid #eee;
}
td {
padding: 15px;
border-bottom: 1px solid #eee;
vertical-align: middle;
}
tr:hover {
background-color: #fcfdfe;
}
/* Buttons - Matching index.html */
.btn {
padding: 12px 20px;
border-radius: 10px;
border: none;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-primary {
background-color: var(--info-color);
color: white;
}
.btn-success {
background-color: var(--success-color);
color: white;
}
.btn-danger {
background-color: var(--danger-color);
color: white;
}
.btn:hover {
opacity: 0.9;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.btn:active {
transform: translateY(0);
}
/* Badges */
.badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
}
.badge-success {
background-color: rgba(46, 204, 113, 0.15);
color: #27ae60;
border: 1px solid #2ecc71;
}
.badge-danger {
background-color: rgba(231, 76, 60, 0.15);
color: #c0392b;
border: 1px solid #e74c3c;
}
/* Notification / Toast */
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 25px;
border-radius: 10px;
color: white;
font-weight: 600;
box-shadow: var(--shadow-heavy);
display: none;
z-index: 1000;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@media (max-width: 768px) {
.inline-form {
grid-template-columns: 1fr;
}
.admin-header {
flex-direction: column;
gap: 15px;
text-align: center;
}
}
+274
View File
@@ -0,0 +1,274 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Outfit', 'Segoe UI', sans-serif;
}
body {
background: linear-gradient(135deg, #1a2980 0%, #26d0ce 100%);
background-attachment: fixed;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
color: #333;
}
.container {
background-color: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
border-radius: 24px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
width: 100%;
max-width: 600px;
padding: 50px 40px;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.4);
}
h1 {
color: #2c3e50;
margin-bottom: 10px;
font-size: 2.2rem;
}
.subtitle {
color: #7f8c8d;
margin-bottom: 30px;
font-size: 1.1rem;
}
.status-panel {
background-color: #f8f9fa;
border-radius: 15px;
padding: 25px;
margin-bottom: 30px;
border: 2px solid #e0e0e0;
text-align: center;
}
.status-title {
font-weight: 600;
color: #555;
margin-bottom: 15px;
font-size: 1.1rem;
}
.status-indicator {
display: inline-flex;
align-items: center;
padding: 12px 25px;
border-radius: 50px;
font-weight: 700;
font-size: 1.3rem;
margin-bottom: 15px;
}
.status-on {
background-color: rgba(46, 204, 113, 0.15);
color: #27ae60;
border: 2px solid #2ecc71;
}
.status-off {
background-color: rgba(231, 76, 60, 0.15);
color: #c0392b;
border: 2px solid #e74c3c;
}
.status-loading {
background-color: rgba(52, 152, 219, 0.15);
color: #2980b9;
border: 2px solid #3498db;
}
.status-disabled {
background-color: rgba(149, 165, 166, 0.15);
color: #7f8c8d;
border: 2px solid #95a5a6;
}
.status-indicator i {
margin-right: 10px;
font-size: 1.5rem;
}
.status-info {
font-size: 0.9rem;
color: #666;
line-height: 1.5;
}
.controls {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 30px;
}
.btn {
padding: 18px 30px;
font-size: 1.2rem;
font-weight: 600;
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 180px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.btn:active {
transform: translateY(2px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.btn-enabled {
opacity: 1;
}
.btn-disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
user-select: none;
}
.btn-on {
background-color: #2ecc71;
color: white;
}
.btn:hover:not(.btn-disabled) {
background-color: #27ae60;
box-shadow: 0 8px 20px rgba(46, 204, 113, 0.4);
transform: translateY(-3px) scale(1.02);
}
.btn-off {
background-color: #e74c3c;
color: white;
}
.btn-off:hover:not(.btn-disabled) {
background-color: #c0392b;
box-shadow: 0 8px 20px rgba(231, 76, 60, 0.4);
transform: translateY(-3px) scale(1.02);
}
.btn i {
margin-right: 10px;
font-size: 1.4rem;
}
.notification {
background-color: rgba(255, 255, 255, 0.4);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border-radius: 12px;
padding: 15px 20px;
margin-top: 20px;
text-align: left;
border-left: 4px solid #3498db;
transition: all 0.3s ease;
display: none;
border: 1px solid rgba(255, 255, 255, 0.3);
border-left-width: 4px;
}
.notification h3 {
color: #2c3e50;
margin-bottom: 15px;
display: flex;
align-items: center;
}
.notification h3 i {
margin-right: 10px;
}
.notification-message {
font-size: 1rem;
line-height: 1.5;
color: #555;
}
.notification-error {
border-left-color: rgba(231, 76, 60, 0.5);
background-color: rgba(231, 76, 60, 0.08);
}
.notification-success {
border-left-color: rgba(46, 204, 113, 0.5);
background-color: rgba(46, 204, 113, 0.08);
}
.notification-warning {
border-left-color: rgba(243, 156, 18, 0.5);
background-color: rgba(243, 156, 18, 0.08);
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(52, 152, 219, 0.3);
border-radius: 50%;
border-top-color: #3498db;
animation: spin 1s ease-in-out infinite;
margin-right: 10px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.api-info {
background-color: rgba(241, 248, 255, 0.6);
backdrop-filter: blur(5px);
border-radius: 12px;
padding: 20px;
margin-top: 30px;
font-size: 0.95rem;
color: #4a5a6a;
text-align: left;
border: 1px solid rgba(52, 152, 219, 0.2);
}
.api-info strong {
color: #2c3e50;
}
.api-info ul {
padding-left: 20px;
margin-top: 8px;
}
.api-info li {
margin-bottom: 5px;
}
@media (max-width: 650px) {
.container {
padding: 30px 20px;
}
.controls {
flex-direction: column;
align-items: center;
}
.btn {
width: 100%;
max-width: 250px;
}
}
+307
View File
@@ -0,0 +1,307 @@
// admin.js
document.addEventListener('DOMContentLoaded', () => {
// Проверка наличия токена перед загрузкой
const token = localStorage.getItem('admin_token');
if (!token) {
window.location.href = '/login';
return;
}
fetchAddresses();
fetchNamedLists();
const addForm = document.getElementById('addAddressForm');
addForm.addEventListener('submit', handleAddAddress);
const addListForm = document.getElementById('addListForm');
addListForm.addEventListener('submit', handleAddList);
const filterInput = document.getElementById('tableFilter');
filterInput.addEventListener('input', handleFilter);
const profileForm = document.getElementById('profileForm');
profileForm.addEventListener('submit', handleProfileUpdate);
});
// Хелпер для авторизованных запросов
async function authorizedFetch(url, options = {}) {
const token = localStorage.getItem('admin_token');
const defaultHeaders = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
const config = {
...options,
headers: {
...defaultHeaders,
...options.headers
}
};
const response = await fetch(url, config);
if (response.status === 401) {
localStorage.removeItem('admin_token');
window.location.href = '/login';
return null;
}
return response;
}
async function fetchNamedLists() {
try {
const response = await authorizedFetch('/api/v1/named-lists');
if (!response) return;
const data = await response.json();
if (data.success) {
renderListsTable(data.data);
populateListDropdown(data.data);
}
} catch (err) {
console.error('Ошибка при загрузке списков:', err);
}
}
function renderListsTable(lists) {
const tbody = document.getElementById('listsTableBody');
tbody.innerHTML = '';
lists.forEach(list => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${list.id}</td>
<td><strong>${list.name}</strong></td>
<td>
<button class="btn btn-danger btn-sm" onclick="deleteNamedList(${list.id})" title="Удалить список">
<i class="fas fa-trash"></i>
</button>
</td>
`;
tbody.appendChild(tr);
});
}
function populateListDropdown(lists) {
const select = document.getElementById('inputList');
const currentValue = select.value;
select.innerHTML = '';
lists.forEach(list => {
const option = document.createElement('option');
option.value = list.name;
option.textContent = list.name;
select.appendChild(option);
});
if (currentValue && lists.some(l => l.name === currentValue)) {
select.value = currentValue;
}
}
async function handleAddList(e) {
e.preventDefault();
const name = document.getElementById('newListName').value;
try {
const response = await authorizedFetch('/api/v1/named-lists', {
method: 'POST',
body: JSON.stringify({ name })
});
if (!response) return;
const data = await response.json();
if (data.success) {
showToast('Список создан', 'success');
document.getElementById('addListForm').reset();
fetchNamedLists();
} else {
showToast('Ошибка: ' + data.error, 'danger');
}
} catch (err) {
showToast('Сетевая ошибка', 'danger');
}
}
async function deleteNamedList(id) {
if (!confirm('Удалить этот список? Это возможно только если в нем нет адресов.')) return;
try {
const response = await authorizedFetch(`/api/v1/named-lists/${id}`, { method: 'DELETE' });
if (!response) return;
const data = await response.json();
if (data.success) {
showToast('Список удален', 'success');
fetchNamedLists();
} else {
showToast('Ошибка: ' + data.error, 'danger');
}
} catch (err) {
showToast('Сетевая ошибка', 'danger');
}
}
async function fetchAddresses() {
try {
const response = await authorizedFetch('/api/v1/addresses');
if (!response) return;
const data = await response.json();
if (data.success) {
renderTable(data.data);
updateStats(data.data);
}
} catch (err) {
showToast('Ошибка при загрузке данных', 'danger');
}
}
function renderTable(addresses) {
const tbody = document.getElementById('addressesTableBody');
tbody.innerHTML = '';
addresses.forEach(item => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${item.id}</td>
<td><strong>${item.ip}</strong></td>
<td><span class="badge" style="background: #eee;">${item.list_name}</span></td>
<td>${item.comment || '-'}</td>
<td>
<span class="badge ${item.enabled ? 'badge-success' : 'badge-danger'}">
${item.enabled ? 'Активен' : 'Отключен'}
</span>
</td>
<td class="actions-cell">
<button class="btn btn-primary btn-sm" onclick="toggleAddress(${item.id}, ${item.enabled})" title="${item.enabled ? 'Отключить' : 'Включить'}">
<i class="fas fa-power-off"></i>
</button>
<button class="btn btn-danger btn-sm" onclick="deleteAddress(${item.id})" title="Удалить">
<i class="fas fa-trash"></i>
</button>
</td>
`;
tbody.appendChild(tr);
});
}
function updateStats(addresses) {
document.getElementById('statTotal').textContent = addresses.length;
document.getElementById('statActive').textContent = addresses.filter(a => a.enabled).length;
document.getElementById('statInactive').textContent = addresses.filter(a => !a.enabled).length;
}
async function handleAddAddress(e) {
e.preventDefault();
const address = document.getElementById('inputAddress').value;
const list = document.getElementById('inputList').value;
const comment = document.getElementById('inputComment').value;
try {
const response = await authorizedFetch('/api/v1/addresses', {
method: 'POST',
body: JSON.stringify({ address, list, comment })
});
if (!response) return;
const data = await response.json();
if (data.success) {
showToast('Адрес успешно добавлен', 'success');
document.getElementById('addAddressForm').reset();
fetchAddresses();
} else {
showToast('Ошибка: ' + data.error, 'danger');
}
} catch (err) {
showToast('Сетевая ошибка', 'danger');
}
}
async function toggleAddress(id, currentEnabled) {
const action = currentEnabled ? 'disable' : 'enable';
try {
const response = await authorizedFetch(`/api/v1/addresses/${id}/${action}`, { method: 'PUT' });
if (!response) return;
const data = await response.json();
if (data.success) {
showToast(`Статус успешно изменен`, 'success');
fetchAddresses();
} else {
showToast('Ошибка: ' + data.error, 'danger');
}
} catch (err) {
showToast('Сетевая ошибка', 'danger');
}
}
async function deleteAddress(id) {
if (!confirm('Вы уверены, что хотите удалить этот адрес?')) return;
try {
const response = await authorizedFetch(`/api/v1/addresses/${id}`, { method: 'DELETE' });
if (!response) return;
const data = await response.json();
if (data.success) {
showToast('Запись удалена', 'success');
fetchAddresses();
} else {
showToast('Ошибка при удалении', 'danger');
}
} catch (err) {
showToast('Сетевая ошибка', 'danger');
}
}
async function handleProfileUpdate(e) {
e.preventDefault();
const username = document.getElementById('profileUsername').value;
const password = document.getElementById('profilePassword').value;
try {
const response = await authorizedFetch('/api/v1/admin/profile', {
method: 'POST',
body: JSON.stringify({ username, password })
});
if (!response) return;
const data = await response.json();
if (data.success) {
showToast('Профиль успешно обновлен. Если вы сменили имя, потребуется перелогиниться.', 'success');
document.getElementById('profilePassword').value = '';
} else {
showToast('Ошибка: ' + data.error, 'danger');
}
} catch (err) {
showToast('Сетевая ошибка', 'danger');
}
}
function handleFilter(e) {
const text = e.target.value.toLowerCase();
const rows = document.querySelectorAll('#addressesTableBody tr');
rows.forEach(row => {
row.style.display = row.textContent.toLowerCase().includes(text) ? '' : 'none';
});
}
function showToast(message, type) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.style.backgroundColor = type === 'success' ? '#2ecc71' : '#e74c3c';
toast.style.display = 'block';
setTimeout(() => {
toast.style.display = 'none';
}, 3000);
}
// Добавим функцию выхода
function logout() {
localStorage.removeItem('admin_token');
window.location.href = '/login';
}
+241
View File
@@ -0,0 +1,241 @@
// Элементы DOM
const statusIndicator = document.getElementById('statusIndicator');
const statusInfo = document.getElementById('statusInfo');
const btnOn = document.getElementById('btnOn');
const btnOff = document.getElementById('btnOff');
const notification = document.getElementById('notification');
const notificationMessage = document.getElementById('notificationMessage');
// Конфигурация API
const API_BASE_URL = window.location.origin;
const API_STATUS_ENDPOINT = `${API_BASE_URL}/api/v1/client/status`;
const API_TOGGLE_ENDPOINT = `${API_BASE_URL}/api/v1/client/toggle`;
// Переменные состояния
let currentStatus = null;
let isRequestInProgress = false;
let hasIntegration = false;
// Инициализация при загрузке страницы
document.addEventListener('DOMContentLoaded', function () {
loadInitialStatus();
btnOn.addEventListener('click', () => sendCommand(true));
btnOff.addEventListener('click', () => sendCommand(false));
});
// Функция загрузки начального статуса
async function loadInitialStatus() {
try {
updateUIForLoading();
const response = await fetch(API_STATUS_ENDPOINT);
const data = await response.json();
if (data.success) {
hasIntegration = true;
currentStatus = data.data.enabled;
updateUIForStatus(currentStatus);
showNotification(`Доступ разрешен. Ваш IP: ${data.data.ip}.`, 'success', true);
} else {
hasIntegration = false;
updateUIForNoIntegration(data.error || 'Нет интеграции с сервисом');
}
} catch (error) {
console.error('Ошибка при загрузке статуса:', error);
updateUIForError('Ошибка соединения с сервером API');
}
}
// Функция отправки команды на сервер
async function sendCommand(enabled) {
if (!hasIntegration || isRequestInProgress) return;
// Прерываем, если запрашиваемое состояние уже активно
if (enabled === currentStatus) return;
isRequestInProgress = true;
const action = enabled ? 'включения' : 'выключения';
updateUIForSendingCommand(enabled);
try {
const response = await fetch(API_TOGGLE_ENDPOINT, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' }
});
const data = await response.json();
if (data.success) {
currentStatus = data.data.enabled;
updateUIForStatus(currentStatus);
showNotification(`Команда ${action} успешно выполнена.`, 'success');
provideVisualFeedback(currentStatus);
} else {
showNotification(`Ошибка: ${data.error}`, 'error');
updateUIForStatus(currentStatus);
}
} catch (error) {
console.error('Ошибка при отправке команды:', error);
showNotification('Ошибка сети при отправке команды', 'error');
updateUIForStatus(currentStatus);
} finally {
isRequestInProgress = false;
}
}
// Обновление UI при загрузке
function updateUIForLoading() {
statusIndicator.className = 'status-indicator status-loading';
statusIndicator.innerHTML = '<i class="fas fa-sync-alt fa-spin"></i> Загрузка...';
statusInfo.textContent = 'Получение текущего состояния с сервера...';
btnOn.classList.add('btn-disabled');
btnOff.classList.add('btn-disabled');
btnOn.disabled = true;
btnOff.disabled = true;
}
// Обновление UI на основе текущего статуса
function updateUIForStatus(enabled) {
if (enabled) {
statusIndicator.className = 'status-indicator status-on';
statusIndicator.innerHTML = '<i class="fas fa-check-circle"></i> Включено';
statusInfo.textContent = 'Устройство активно и работает в нормальном режиме';
btnOn.classList.add('btn-disabled');
btnOn.disabled = true;
btnOff.classList.remove('btn-disabled');
btnOff.disabled = false;
} else {
statusIndicator.className = 'status-indicator status-off';
statusIndicator.innerHTML = '<i class="fas fa-power-off"></i> Выключено';
statusInfo.textContent = 'Устройство отключено и не выполняет свои функции';
btnOff.classList.add('btn-disabled');
btnOff.disabled = true;
btnOn.classList.remove('btn-disabled');
btnOn.disabled = false;
}
}
// Обновление UI при отсутствии интеграции
function updateUIForNoIntegration(message) {
statusIndicator.className = 'status-indicator status-disabled';
statusIndicator.innerHTML = '<i class="fas fa-ban"></i> Нет доступа';
statusInfo.textContent = 'Ваш IP-адрес не найден в списке доверенных адресов';
btnOn.classList.add('btn-disabled');
btnOff.classList.add('btn-disabled');
btnOn.disabled = true;
btnOff.disabled = true;
showNotification(message, 'error', true);
}
// Обновление UI при ошибке
function updateUIForError(errorMessage) {
statusIndicator.className = 'status-indicator status-disabled';
statusIndicator.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Ошибка';
statusInfo.textContent = 'Не удалось получить информацию о статусе устройства';
btnOn.classList.add('btn-disabled');
btnOff.classList.add('btn-disabled');
btnOn.disabled = true;
btnOff.disabled = true;
showNotification(errorMessage, 'error', true);
}
// Обновление UI при отправке команды
function updateUIForSendingCommand(enabled) {
const action = enabled ? 'включения' : 'выключения';
statusIndicator.className = 'status-indicator status-loading';
statusIndicator.innerHTML = '<i class="fas fa-sync-alt fa-spin"></i> Отправка...';
statusInfo.textContent = `Отправка команды ${action} на сервер...`;
btnOn.classList.add('btn-disabled');
btnOff.classList.add('btn-disabled');
btnOn.disabled = true;
btnOff.disabled = true;
showNotification(`Отправка команды ${action}...`, 'warning');
}
// Переменные для управления уведомлениями
let baseNotificationContent = null;
let notificationTimeout = null;
// Показать уведомление
function showNotification(message, type = 'info', isPersistent = false) {
if (notificationTimeout) {
clearTimeout(notificationTimeout);
notificationTimeout = null;
}
notification.className = 'notification';
let icon = 'fa-info-circle';
let title = 'Информация:';
if (type === 'error') {
notification.classList.add('notification-error');
icon = 'fa-exclamation-circle';
title = 'Ошибка:';
} else if (type === 'success') {
notification.classList.add('notification-success');
icon = 'fa-check-circle';
title = 'Успешно:';
} else if (type === 'warning') {
notification.classList.add('notification-warning');
icon = 'fa-sync-alt fa-spin';
title = 'Обработка:';
}
const content = `<h3><i class="fas ${icon}"></i> ${title}</h3><div class="notification-message">${message}</div>`;
notification.innerHTML = content;
if (isPersistent) {
baseNotificationContent = content;
}
notification.style.opacity = '0';
notification.style.display = 'block';
// Плавное появление
setTimeout(() => {
notification.style.transition = 'opacity 0.3s ease';
notification.style.opacity = '1';
}, 10);
// Авто-возврат к базовому уведомлению через 5 секунд (если это не само базовое и не "в обработке")
if (!isPersistent && baseNotificationContent && type !== 'warning') {
notificationTimeout = setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => {
notification.className = 'notification';
notification.innerHTML = baseNotificationContent;
notification.style.opacity = '1';
}, 300);
}, 5000);
}
}
// Визуальная обратная связь
function provideVisualFeedback(enabled) {
if (enabled) {
document.body.style.background = 'linear-gradient(135deg, #2ecc71 0%, #27ae60 100%)';
setTimeout(() => {
document.body.style.background = 'linear-gradient(135deg, #1a2980 0%, #26d0ce 100%)';
}, 800);
} else {
document.body.style.background = 'linear-gradient(135deg, #e74c3c 0%, #c0392b 100%)';
setTimeout(() => {
document.body.style.background = 'linear-gradient(135deg, #1a2980 0%, #26d0ce 100%)';
}, 800);
}
}
// Дополнительная функция для ручного обновления статуса
function refreshStatus() {
if (!isRequestInProgress) {
loadInitialStatus();
}
}
statusIndicator.addEventListener('dblclick', refreshStatus);
+58
View File
@@ -0,0 +1,58 @@
// login.js
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const loginBtn = document.getElementById('loginBtn');
// UI Feedback
loginBtn.classList.add('btn-disabled');
loginBtn.innerHTML = '<i class="fas fa-sync-alt fa-spin"></i> Авторизация...';
try {
const response = await fetch('/api/v1/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success) {
// Сохраняем токен
localStorage.setItem('admin_token', data.data.token);
showStatus('Успешно! Вход в систему...', 'success');
setTimeout(() => {
window.location.href = '/admin';
}, 800);
} else {
showStatus(data.error || 'Ошибка входа', 'error');
resetButton(loginBtn);
}
} catch (err) {
showStatus('Ошибка сети или сервера', 'error');
resetButton(loginBtn);
}
});
function resetButton(btn) {
btn.classList.remove('btn-disabled');
btn.innerHTML = '<i class="fas fa-sign-in-alt"></i> Войти в панель';
}
function showStatus(message, type) {
const notification = document.getElementById('notification');
notification.className = 'notification';
if (type === 'error') {
notification.classList.add('notification-error');
notification.innerHTML = `<h3><i class="fas fa-exclamation-circle"></i> Ошибка:</h3><div class="notification-message">${message}</div>`;
} else {
notification.classList.add('notification-success');
notification.innerHTML = `<h3><i class="fas fa-check-circle"></i> Успешно:</h3><div class="notification-message">${message}</div>`;
}
notification.style.display = 'block';
notification.style.opacity = '1';
}