сборка для Ильи
This commit is contained in:
commit
59ce64be5c
18 files changed
+2400
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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';
|
||||
}
|
||||
Reference in new issue
Block a user