Большое обновление GUI интерфейс
Большое обновление GUI интерфейс - Добавлен фраемворr Walles - Удалена консольная версия - Проработан интерфейс и дизайн - Добавлено кеширование для быстрой реакции. - Сделан .ps1 сборщик для удобной сборки проекта. - Обновлён Readme
This commit is contained in:
129
Backend/admin/frontend/assets/js/api/config.js
Normal file
129
Backend/admin/frontend/assets/js/api/config.js
Normal file
@@ -0,0 +1,129 @@
|
||||
/* ============================================
|
||||
Config API
|
||||
Работа с конфигурацией
|
||||
============================================ */
|
||||
|
||||
import { isWailsAvailable, log } from '../utils/helpers.js';
|
||||
|
||||
/**
|
||||
* Класс для работы с конфигурацией
|
||||
*/
|
||||
class ConfigAPI {
|
||||
constructor() {
|
||||
this.available = isWailsAvailable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить конфигурацию
|
||||
*/
|
||||
async getConfig() {
|
||||
if (!this.available) return null;
|
||||
try {
|
||||
return await window.go.admin.App.GetConfig();
|
||||
} catch (error) {
|
||||
log(`Ошибка получения конфигурации: ${error.message}`, 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить конфигурацию
|
||||
*/
|
||||
async saveConfig(configJSON) {
|
||||
if (!this.available) return 'Error: API недоступен';
|
||||
try {
|
||||
return await window.go.admin.App.SaveConfig(configJSON);
|
||||
} catch (error) {
|
||||
log(`Ошибка сохранения конфигурации: ${error.message}`, 'error');
|
||||
return `Error: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Включить Proxy Service
|
||||
*/
|
||||
async enableProxyService() {
|
||||
if (!this.available) return;
|
||||
try {
|
||||
await window.go.admin.App.EnableProxyService();
|
||||
} catch (error) {
|
||||
log(`Ошибка включения Proxy: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отключить Proxy Service
|
||||
*/
|
||||
async disableProxyService() {
|
||||
if (!this.available) return;
|
||||
try {
|
||||
await window.go.admin.App.DisableProxyService();
|
||||
} catch (error) {
|
||||
log(`Ошибка отключения Proxy: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Перезапустить все сервисы
|
||||
*/
|
||||
async restartAllServices() {
|
||||
if (!this.available) return;
|
||||
try {
|
||||
await window.go.admin.App.RestartAllServices();
|
||||
} catch (error) {
|
||||
log(`Ошибка перезапуска сервисов: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запустить HTTP Service
|
||||
*/
|
||||
async startHTTPService() {
|
||||
if (!this.available) return;
|
||||
try {
|
||||
await window.go.admin.App.StartHTTPService();
|
||||
} catch (error) {
|
||||
log(`Ошибка запуска HTTP: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановить HTTP Service
|
||||
*/
|
||||
async stopHTTPService() {
|
||||
if (!this.available) return;
|
||||
try {
|
||||
await window.go.admin.App.StopHTTPService();
|
||||
} catch (error) {
|
||||
log(`Ошибка остановки HTTP: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запустить HTTPS Service
|
||||
*/
|
||||
async startHTTPSService() {
|
||||
if (!this.available) return;
|
||||
try {
|
||||
await window.go.admin.App.StartHTTPSService();
|
||||
} catch (error) {
|
||||
log(`Ошибка запуска HTTPS: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановить HTTPS Service
|
||||
*/
|
||||
async stopHTTPSService() {
|
||||
if (!this.available) return;
|
||||
try {
|
||||
await window.go.admin.App.StopHTTPSService();
|
||||
} catch (error) {
|
||||
log(`Ошибка остановки HTTPS: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Экспортируем единственный экземпляр
|
||||
export const configAPI = new ConfigAPI();
|
||||
|
||||
143
Backend/admin/frontend/assets/js/api/wails.js
Normal file
143
Backend/admin/frontend/assets/js/api/wails.js
Normal file
@@ -0,0 +1,143 @@
|
||||
/* ============================================
|
||||
Wails API Wrapper
|
||||
Обёртка над Wails API
|
||||
============================================ */
|
||||
|
||||
import { isWailsAvailable, log } from '../utils/helpers.js';
|
||||
|
||||
/**
|
||||
* Базовый класс для работы с Wails API
|
||||
*/
|
||||
class WailsAPI {
|
||||
constructor() {
|
||||
this.available = isWailsAvailable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка доступности API
|
||||
*/
|
||||
checkAvailability() {
|
||||
if (!this.available) {
|
||||
log('Wails API недоступен', 'warn');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить статус всех сервисов
|
||||
*/
|
||||
async getAllServicesStatus() {
|
||||
if (!this.checkAvailability()) return null;
|
||||
try {
|
||||
return await window.go.admin.App.GetAllServicesStatus();
|
||||
} catch (error) {
|
||||
log(`Ошибка получения статуса сервисов: ${error.message}`, 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить список сайтов
|
||||
*/
|
||||
async getSitesList() {
|
||||
if (!this.checkAvailability()) return [];
|
||||
try {
|
||||
return await window.go.admin.App.GetSitesList();
|
||||
} catch (error) {
|
||||
log(`Ошибка получения списка сайтов: ${error.message}`, 'error');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить список прокси
|
||||
*/
|
||||
async getProxyList() {
|
||||
if (!this.checkAvailability()) return [];
|
||||
try {
|
||||
return await window.go.admin.App.GetProxyList();
|
||||
} catch (error) {
|
||||
log(`Ошибка получения списка прокси: ${error.message}`, 'error');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить правила vAccess
|
||||
*/
|
||||
async getVAccessRules(host, isProxy) {
|
||||
if (!this.checkAvailability()) return { rules: [] };
|
||||
try {
|
||||
return await window.go.admin.App.GetVAccessRules(host, isProxy);
|
||||
} catch (error) {
|
||||
log(`Ошибка получения правил vAccess: ${error.message}`, 'error');
|
||||
return { rules: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить правила vAccess
|
||||
*/
|
||||
async saveVAccessRules(host, isProxy, configJSON) {
|
||||
if (!this.checkAvailability()) return 'Error: API недоступен';
|
||||
try {
|
||||
return await window.go.admin.App.SaveVAccessRules(host, isProxy, configJSON);
|
||||
} catch (error) {
|
||||
log(`Ошибка сохранения правил vAccess: ${error.message}`, 'error');
|
||||
return `Error: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запустить сервер
|
||||
*/
|
||||
async startServer() {
|
||||
if (!this.checkAvailability()) return;
|
||||
try {
|
||||
await window.go.admin.App.StartServer();
|
||||
} catch (error) {
|
||||
log(`Ошибка запуска сервера: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановить сервер
|
||||
*/
|
||||
async stopServer() {
|
||||
if (!this.checkAvailability()) return;
|
||||
try {
|
||||
await window.go.admin.App.StopServer();
|
||||
} catch (error) {
|
||||
log(`Ошибка остановки сервера: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить готовность сервисов
|
||||
*/
|
||||
async checkServicesReady() {
|
||||
if (!this.checkAvailability()) return false;
|
||||
try {
|
||||
return await window.go.admin.App.CheckServicesReady();
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Открыть папку сайта
|
||||
*/
|
||||
async openSiteFolder(host) {
|
||||
if (!this.checkAvailability()) return;
|
||||
try {
|
||||
await window.go.admin.App.OpenSiteFolder(host);
|
||||
} catch (error) {
|
||||
log(`Ошибка открытия папки: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Экспортируем единственный экземпляр
|
||||
export const api = new WailsAPI();
|
||||
|
||||
151
Backend/admin/frontend/assets/js/components/proxy.js
Normal file
151
Backend/admin/frontend/assets/js/components/proxy.js
Normal file
@@ -0,0 +1,151 @@
|
||||
/* ============================================
|
||||
Proxy Component
|
||||
Управление прокси
|
||||
============================================ */
|
||||
|
||||
import { api } from '../api/wails.js';
|
||||
import { isWailsAvailable } from '../utils/helpers.js';
|
||||
import { $ } from '../utils/dom.js';
|
||||
|
||||
/**
|
||||
* Класс для управления прокси
|
||||
*/
|
||||
export class ProxyManager {
|
||||
constructor() {
|
||||
this.proxiesData = [];
|
||||
this.mockData = [
|
||||
{
|
||||
enable: true,
|
||||
external_domain: 'git.example.ru',
|
||||
local_address: '127.0.0.1',
|
||||
local_port: '3333',
|
||||
service_https_use: false,
|
||||
auto_https: true,
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
enable: true,
|
||||
external_domain: 'api.example.com',
|
||||
local_address: '127.0.0.1',
|
||||
local_port: '8080',
|
||||
service_https_use: true,
|
||||
auto_https: false,
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
enable: false,
|
||||
external_domain: 'test.example.net',
|
||||
local_address: '127.0.0.1',
|
||||
local_port: '5000',
|
||||
service_https_use: false,
|
||||
auto_https: false,
|
||||
status: 'disabled'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузить список прокси
|
||||
*/
|
||||
async load() {
|
||||
if (isWailsAvailable()) {
|
||||
this.proxiesData = await api.getProxyList();
|
||||
} else {
|
||||
// Используем тестовые данные если Wails недоступен
|
||||
this.proxiesData = this.mockData;
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Отрисовать список прокси
|
||||
*/
|
||||
render() {
|
||||
const tbody = $('proxyTable')?.querySelector('tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
this.proxiesData.forEach((proxy, index) => {
|
||||
const row = document.createElement('tr');
|
||||
const statusBadge = proxy.status === 'active' ? 'badge-online' : 'badge-offline';
|
||||
const httpsBadge = proxy.service_https_use ? 'badge-yes">HTTPS' : 'badge-no">HTTP';
|
||||
const autoHttpsBadge = proxy.auto_https ? 'badge-yes">Да' : 'badge-no">Нет';
|
||||
const protocol = proxy.auto_https ? 'https' : 'http';
|
||||
|
||||
row.innerHTML = `
|
||||
<td><code class="clickable-link" data-url="${protocol}://${proxy.external_domain}">${proxy.external_domain} <i class="fas fa-external-link-alt"></i></code></td>
|
||||
<td><code>${proxy.local_address}:${proxy.local_port}</code></td>
|
||||
<td><span class="badge ${httpsBadge}</span></td>
|
||||
<td><span class="badge ${autoHttpsBadge}</span></td>
|
||||
<td><span class="badge ${statusBadge}">${proxy.status}</span></td>
|
||||
<td>
|
||||
<button class="icon-btn" data-action="edit-vaccess" data-host="${proxy.external_domain}" data-is-proxy="true" title="vAccess"><i class="fas fa-shield-alt"></i></button>
|
||||
<button class="icon-btn" data-action="edit-proxy" data-index="${index}" title="Редактировать"><i class="fas fa-edit"></i></button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
// Добавляем обработчики событий
|
||||
this.attachEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить обработчики событий
|
||||
*/
|
||||
attachEventListeners() {
|
||||
// Кликабельные ссылки
|
||||
const links = document.querySelectorAll('.clickable-link[data-url]');
|
||||
links.forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
const url = link.getAttribute('data-url');
|
||||
this.openLink(url);
|
||||
});
|
||||
});
|
||||
|
||||
// Кнопки действий
|
||||
const buttons = document.querySelectorAll('[data-action]');
|
||||
buttons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const action = btn.getAttribute('data-action');
|
||||
this.handleAction(action, btn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработчик действий
|
||||
*/
|
||||
handleAction(action, btn) {
|
||||
const host = btn.getAttribute('data-host');
|
||||
const index = parseInt(btn.getAttribute('data-index'));
|
||||
const isProxy = btn.getAttribute('data-is-proxy') === 'true';
|
||||
|
||||
switch (action) {
|
||||
case 'edit-vaccess':
|
||||
if (window.editVAccess) {
|
||||
window.editVAccess(host, isProxy);
|
||||
}
|
||||
break;
|
||||
case 'edit-proxy':
|
||||
if (window.editProxy) {
|
||||
window.editProxy(index);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Открыть ссылку
|
||||
*/
|
||||
openLink(url) {
|
||||
if (window.runtime?.BrowserOpenURL) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
} else {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
191
Backend/admin/frontend/assets/js/components/services.js
Normal file
191
Backend/admin/frontend/assets/js/components/services.js
Normal file
@@ -0,0 +1,191 @@
|
||||
/* ============================================
|
||||
Services Component
|
||||
Управление сервисами
|
||||
============================================ */
|
||||
|
||||
import { api } from '../api/wails.js';
|
||||
import { $, $$, addClass, removeClass } from '../utils/dom.js';
|
||||
import { notification } from '../ui/notification.js';
|
||||
import { sleep, isWailsAvailable } from '../utils/helpers.js';
|
||||
|
||||
/**
|
||||
* Класс для управления сервисами
|
||||
*/
|
||||
export class ServicesManager {
|
||||
constructor() {
|
||||
this.serverRunning = true;
|
||||
this.isOperating = false;
|
||||
this.controlBtn = $('serverControlBtn');
|
||||
this.statusIndicator = document.querySelector('.status-indicator');
|
||||
this.statusText = document.querySelector('.status-text');
|
||||
this.btnText = document.querySelector('.btn-text');
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.controlBtn) {
|
||||
this.controlBtn.addEventListener('click', () => this.toggleServer());
|
||||
}
|
||||
|
||||
// Подписка на события
|
||||
if (window.runtime?.EventsOn) {
|
||||
window.runtime.EventsOn('service:changed', (status) => {
|
||||
this.renderServices(status);
|
||||
});
|
||||
|
||||
window.runtime.EventsOn('server:already_running', () => {
|
||||
notification.error('vServer уже запущен!<br><br>Закройте другой экземпляр перед запуском нового.', 5000);
|
||||
this.setServerStatus(false, 'Уже запущен в другом процессе');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключить состояние сервера
|
||||
*/
|
||||
async toggleServer() {
|
||||
if (this.serverRunning) {
|
||||
await this.stopServer();
|
||||
} else {
|
||||
await this.startServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запустить сервер
|
||||
*/
|
||||
async startServer() {
|
||||
this.isOperating = true;
|
||||
this.controlBtn.disabled = true;
|
||||
this.statusText.textContent = 'Запускается...';
|
||||
this.btnText.textContent = 'Ожидайте...';
|
||||
this.setAllServicesPending('Запуск');
|
||||
|
||||
await api.startServer();
|
||||
|
||||
// Ждём пока все запустятся
|
||||
let attempts = 0;
|
||||
while (attempts < 20) {
|
||||
await sleep(500);
|
||||
if (await api.checkServicesReady()) {
|
||||
break;
|
||||
}
|
||||
attempts++;
|
||||
}
|
||||
|
||||
this.isOperating = false;
|
||||
this.setServerStatus(true, 'Сервер запущен');
|
||||
removeClass(this.controlBtn, 'start-mode');
|
||||
this.btnText.textContent = 'Остановить';
|
||||
this.controlBtn.disabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановить сервер
|
||||
*/
|
||||
async stopServer() {
|
||||
this.isOperating = true;
|
||||
this.controlBtn.disabled = true;
|
||||
this.statusText.textContent = 'Выключается...';
|
||||
this.btnText.textContent = 'Ожидайте...';
|
||||
this.setAllServicesPending('Остановка');
|
||||
|
||||
await api.stopServer();
|
||||
await sleep(1500);
|
||||
|
||||
this.isOperating = false;
|
||||
this.setServerStatus(false, 'Сервер остановлен');
|
||||
addClass(this.controlBtn, 'start-mode');
|
||||
this.btnText.textContent = 'Запустить';
|
||||
this.controlBtn.disabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Установить статус сервера
|
||||
*/
|
||||
setServerStatus(isOnline, text) {
|
||||
this.serverRunning = isOnline;
|
||||
|
||||
if (isOnline) {
|
||||
removeClass(this.statusIndicator, 'status-offline');
|
||||
addClass(this.statusIndicator, 'status-online');
|
||||
} else {
|
||||
removeClass(this.statusIndicator, 'status-online');
|
||||
addClass(this.statusIndicator, 'status-offline');
|
||||
}
|
||||
|
||||
this.statusText.textContent = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Установить всем сервисам статус pending
|
||||
*/
|
||||
setAllServicesPending(text) {
|
||||
const badges = $$('.service-card .badge');
|
||||
badges.forEach(badge => {
|
||||
badge.className = 'badge badge-pending';
|
||||
badge.textContent = text;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Отрисовать статусы сервисов
|
||||
*/
|
||||
renderServices(data) {
|
||||
const services = [data.http, data.https, data.mysql, data.php, data.proxy];
|
||||
const cards = $$('.service-card');
|
||||
|
||||
services.forEach((service, index) => {
|
||||
const card = cards[index];
|
||||
if (!card) return;
|
||||
|
||||
const badge = card.querySelector('.badge');
|
||||
const infoValues = card.querySelectorAll('.info-value');
|
||||
|
||||
// Обновляем badge только если НЕ в процессе операции
|
||||
if (badge && !this.isOperating) {
|
||||
if (service.status) {
|
||||
badge.className = 'badge badge-online';
|
||||
badge.textContent = 'Активен';
|
||||
} else {
|
||||
badge.className = 'badge badge-offline';
|
||||
badge.textContent = 'Остановлен';
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем значения
|
||||
if (service.name === 'Proxy') {
|
||||
if (infoValues[0] && service.info) {
|
||||
infoValues[0].textContent = service.info;
|
||||
}
|
||||
} else {
|
||||
if (infoValues[0]) {
|
||||
infoValues[0].textContent = service.port;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузить статусы сервисов
|
||||
*/
|
||||
async loadStatus() {
|
||||
if (isWailsAvailable()) {
|
||||
const data = await api.getAllServicesStatus();
|
||||
if (data) {
|
||||
this.renderServices(data);
|
||||
}
|
||||
} else {
|
||||
// Используем тестовые данные если Wails недоступен
|
||||
const mockData = {
|
||||
http: { name: 'HTTP', status: true, port: '80' },
|
||||
https: { name: 'HTTPS', status: true, port: '443' },
|
||||
mysql: { name: 'MySQL', status: true, port: '3306' },
|
||||
php: { name: 'PHP', status: true, port: '8000-8003' },
|
||||
proxy: { name: 'Proxy', status: true, port: '', info: '1 из 3' }
|
||||
};
|
||||
this.renderServices(mockData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
150
Backend/admin/frontend/assets/js/components/sites.js
Normal file
150
Backend/admin/frontend/assets/js/components/sites.js
Normal file
@@ -0,0 +1,150 @@
|
||||
/* ============================================
|
||||
Sites Component
|
||||
Управление сайтами
|
||||
============================================ */
|
||||
|
||||
import { api } from '../api/wails.js';
|
||||
import { isWailsAvailable } from '../utils/helpers.js';
|
||||
import { $ } from '../utils/dom.js';
|
||||
|
||||
/**
|
||||
* Класс для управления сайтами
|
||||
*/
|
||||
export class SitesManager {
|
||||
constructor() {
|
||||
this.sitesData = [];
|
||||
this.mockData = [
|
||||
{
|
||||
name: 'Локальный сайт',
|
||||
host: '127.0.0.1',
|
||||
alias: ['localhost'],
|
||||
status: 'active',
|
||||
root_file: 'index.html',
|
||||
root_file_routing: true
|
||||
},
|
||||
{
|
||||
name: 'Тестовый проект',
|
||||
host: 'test.local',
|
||||
alias: ['*.test.local', 'test.com'],
|
||||
status: 'active',
|
||||
root_file: 'index.php',
|
||||
root_file_routing: false
|
||||
},
|
||||
{
|
||||
name: 'API сервис',
|
||||
host: 'api.example.com',
|
||||
alias: ['*.api.example.com'],
|
||||
status: 'inactive',
|
||||
root_file: 'index.php',
|
||||
root_file_routing: true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузить список сайтов
|
||||
*/
|
||||
async load() {
|
||||
if (isWailsAvailable()) {
|
||||
this.sitesData = await api.getSitesList();
|
||||
} else {
|
||||
// Используем тестовые данные если Wails недоступен
|
||||
this.sitesData = this.mockData;
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Отрисовать список сайтов
|
||||
*/
|
||||
render() {
|
||||
const tbody = $('sitesTable')?.querySelector('tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
this.sitesData.forEach((site, index) => {
|
||||
const row = document.createElement('tr');
|
||||
const statusBadge = site.status === 'active' ? 'badge-online' : 'badge-offline';
|
||||
const aliases = site.alias.join(', ');
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${site.name}</td>
|
||||
<td><code class="clickable-link" data-url="http://${site.host}">${site.host} <i class="fas fa-external-link-alt"></i></code></td>
|
||||
<td><code>${aliases}</code></td>
|
||||
<td><span class="badge ${statusBadge}">${site.status}</span></td>
|
||||
<td><code>${site.root_file}</code></td>
|
||||
<td>
|
||||
<button class="icon-btn" data-action="open-folder" data-host="${site.host}" title="Открыть папку"><i class="fas fa-folder-open"></i></button>
|
||||
<button class="icon-btn" data-action="edit-vaccess" data-host="${site.host}" data-is-proxy="false" title="vAccess"><i class="fas fa-shield-alt"></i></button>
|
||||
<button class="icon-btn" data-action="edit-site" data-index="${index}" title="Редактировать"><i class="fas fa-edit"></i></button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
// Добавляем обработчики событий
|
||||
this.attachEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить обработчики событий
|
||||
*/
|
||||
attachEventListeners() {
|
||||
// Кликабельные ссылки
|
||||
const links = document.querySelectorAll('.clickable-link[data-url]');
|
||||
links.forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
const url = link.getAttribute('data-url');
|
||||
this.openLink(url);
|
||||
});
|
||||
});
|
||||
|
||||
// Кнопки действий
|
||||
const buttons = document.querySelectorAll('[data-action]');
|
||||
buttons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const action = btn.getAttribute('data-action');
|
||||
this.handleAction(action, btn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработчик действий
|
||||
*/
|
||||
async handleAction(action, btn) {
|
||||
const host = btn.getAttribute('data-host');
|
||||
const index = parseInt(btn.getAttribute('data-index'));
|
||||
const isProxy = btn.getAttribute('data-is-proxy') === 'true';
|
||||
|
||||
switch (action) {
|
||||
case 'open-folder':
|
||||
await api.openSiteFolder(host);
|
||||
break;
|
||||
case 'edit-vaccess':
|
||||
if (window.editVAccess) {
|
||||
window.editVAccess(host, isProxy);
|
||||
}
|
||||
break;
|
||||
case 'edit-site':
|
||||
if (window.editSite) {
|
||||
window.editSite(index);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Открыть ссылку
|
||||
*/
|
||||
openLink(url) {
|
||||
if (window.runtime?.BrowserOpenURL) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
} else {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
396
Backend/admin/frontend/assets/js/components/vaccess.js
Normal file
396
Backend/admin/frontend/assets/js/components/vaccess.js
Normal file
@@ -0,0 +1,396 @@
|
||||
/* ============================================
|
||||
vAccess Component
|
||||
Управление правилами доступа
|
||||
============================================ */
|
||||
|
||||
import { api } from '../api/wails.js';
|
||||
import { $, hide, show } from '../utils/dom.js';
|
||||
import { notification } from '../ui/notification.js';
|
||||
import { modal } from '../ui/modal.js';
|
||||
import { isWailsAvailable } from '../utils/helpers.js';
|
||||
|
||||
/**
|
||||
* Класс для управления vAccess правилами
|
||||
*/
|
||||
export class VAccessManager {
|
||||
constructor() {
|
||||
this.vAccessHost = '';
|
||||
this.vAccessIsProxy = false;
|
||||
this.vAccessRules = [];
|
||||
this.vAccessReturnSection = 'sectionSites';
|
||||
this.draggedIndex = null;
|
||||
this.editingField = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Открыть редактор vAccess
|
||||
*/
|
||||
async open(host, isProxy) {
|
||||
this.vAccessHost = host;
|
||||
this.vAccessIsProxy = isProxy;
|
||||
|
||||
// Запоминаем откуда пришли
|
||||
if ($('sectionSites').style.display !== 'none') {
|
||||
this.vAccessReturnSection = 'sectionSites';
|
||||
} else if ($('sectionProxy').style.display !== 'none') {
|
||||
this.vAccessReturnSection = 'sectionProxy';
|
||||
}
|
||||
|
||||
// Загружаем правила
|
||||
if (isWailsAvailable()) {
|
||||
const config = await api.getVAccessRules(host, isProxy);
|
||||
this.vAccessRules = config.rules || [];
|
||||
} else {
|
||||
// Тестовые данные для браузерного режима
|
||||
this.vAccessRules = [
|
||||
{
|
||||
type: 'Disable',
|
||||
type_file: ['*.php'],
|
||||
path_access: ['/uploads/*'],
|
||||
ip_list: [],
|
||||
exceptions_dir: [],
|
||||
url_error: '404'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Обновляем UI
|
||||
const subtitle = isProxy
|
||||
? 'Управление правилами доступа для прокси-сервиса'
|
||||
: 'Управление правилами доступа для сайта';
|
||||
|
||||
$('breadcrumbHost').textContent = host;
|
||||
$('vAccessSubtitle').textContent = subtitle;
|
||||
|
||||
// Переключаем на страницу редактора
|
||||
this.hideAllSections();
|
||||
show($('sectionVAccessEditor'));
|
||||
|
||||
// Рендерим правила и показываем правильную вкладку
|
||||
this.renderRulesList();
|
||||
this.switchTab('rules');
|
||||
|
||||
// Привязываем кнопку сохранения
|
||||
const saveBtn = $('saveVAccessBtn');
|
||||
if (saveBtn) {
|
||||
saveBtn.onclick = async () => await this.save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Скрыть все секции
|
||||
*/
|
||||
hideAllSections() {
|
||||
hide($('sectionServices'));
|
||||
hide($('sectionSites'));
|
||||
hide($('sectionProxy'));
|
||||
hide($('sectionSettings'));
|
||||
hide($('sectionVAccessEditor'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Вернуться на главную
|
||||
*/
|
||||
backToMain() {
|
||||
this.hideAllSections();
|
||||
show($('sectionServices'));
|
||||
show($('sectionSites'));
|
||||
show($('sectionProxy'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключить вкладку
|
||||
*/
|
||||
switchTab(tab) {
|
||||
const tabs = document.querySelectorAll('.vaccess-tab[data-tab]');
|
||||
tabs.forEach(t => {
|
||||
if (t.dataset.tab === tab) {
|
||||
t.classList.add('active');
|
||||
} else {
|
||||
t.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
if (tab === 'rules') {
|
||||
show($('vAccessRulesTab'));
|
||||
hide($('vAccessHelpTab'));
|
||||
} else {
|
||||
hide($('vAccessRulesTab'));
|
||||
show($('vAccessHelpTab'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить изменения
|
||||
*/
|
||||
async save() {
|
||||
if (isWailsAvailable()) {
|
||||
const config = { rules: this.vAccessRules };
|
||||
const configJSON = JSON.stringify(config);
|
||||
const result = await api.saveVAccessRules(this.vAccessHost, this.vAccessIsProxy, configJSON);
|
||||
|
||||
if (result.startsWith('Error')) {
|
||||
notification.error(result, 2000);
|
||||
} else {
|
||||
notification.success('✅ Правила vAccess успешно сохранены', 1000);
|
||||
}
|
||||
} else {
|
||||
// Браузерный режим - просто показываем уведомление
|
||||
notification.success('Данные сохранены (тестовый режим)');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отрисовать список правил
|
||||
*/
|
||||
renderRulesList() {
|
||||
const tbody = $('vAccessTableBody');
|
||||
const emptyState = $('vAccessEmpty');
|
||||
const table = document.querySelector('.vaccess-table');
|
||||
|
||||
if (!tbody) return;
|
||||
|
||||
// Показываем/скрываем пустое состояние
|
||||
if (this.vAccessRules.length === 0) {
|
||||
if (table) hide(table);
|
||||
if (emptyState) show(emptyState);
|
||||
return;
|
||||
} else {
|
||||
if (table) show(table);
|
||||
if (emptyState) hide(emptyState);
|
||||
}
|
||||
|
||||
tbody.innerHTML = this.vAccessRules.map((rule, index) => `
|
||||
<tr draggable="true" data-index="${index}">
|
||||
<td class="drag-handle"><i class="fas fa-grip-vertical"></i></td>
|
||||
<td data-field="type" data-index="${index}">
|
||||
<span class="badge ${rule.type === 'Allow' ? 'badge-yes' : 'badge-no'}">${rule.type}</span>
|
||||
</td>
|
||||
<td data-field="type_file" data-index="${index}">
|
||||
${(rule.type_file || []).length > 0 ? (rule.type_file || []).map(f => `<code class="mini-tag">${f}</code>`).join(' ') : '<span class="empty-field">-</span>'}
|
||||
</td>
|
||||
<td data-field="path_access" data-index="${index}">
|
||||
${(rule.path_access || []).length > 0 ? (rule.path_access || []).map(p => `<code class="mini-tag">${p}</code>`).join(' ') : '<span class="empty-field">-</span>'}
|
||||
</td>
|
||||
<td data-field="ip_list" data-index="${index}">
|
||||
${(rule.ip_list || []).length > 0 ? (rule.ip_list || []).map(ip => `<code class="mini-tag">${ip}</code>`).join(' ') : '<span class="empty-field">-</span>'}
|
||||
</td>
|
||||
<td data-field="exceptions_dir" data-index="${index}">
|
||||
${(rule.exceptions_dir || []).length > 0 ? (rule.exceptions_dir || []).map(e => `<code class="mini-tag">${e}</code>`).join(' ') : '<span class="empty-field">-</span>'}
|
||||
</td>
|
||||
<td data-field="url_error" data-index="${index}">
|
||||
<code class="mini-tag">${rule.url_error || '404'}</code>
|
||||
</td>
|
||||
<td>
|
||||
<button class="icon-btn-small" data-action="remove-rule" data-index="${index}" title="Удалить"><i class="fas fa-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
// Добавляем обработчики
|
||||
this.attachRulesEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить обработчики событий для правил
|
||||
*/
|
||||
attachRulesEventListeners() {
|
||||
// Drag & Drop
|
||||
const rows = document.querySelectorAll('#vAccessTableBody tr[draggable]');
|
||||
rows.forEach(row => {
|
||||
row.addEventListener('dragstart', (e) => this.onDragStart(e));
|
||||
row.addEventListener('dragover', (e) => this.onDragOver(e));
|
||||
row.addEventListener('drop', (e) => this.onDrop(e));
|
||||
});
|
||||
|
||||
// Клик по ячейкам для редактирования
|
||||
const cells = document.querySelectorAll('#vAccessTableBody td[data-field]');
|
||||
cells.forEach(cell => {
|
||||
cell.addEventListener('click', () => {
|
||||
const field = cell.getAttribute('data-field');
|
||||
const index = parseInt(cell.getAttribute('data-index'));
|
||||
this.editRuleField(index, field);
|
||||
});
|
||||
});
|
||||
|
||||
// Кнопки удаления
|
||||
const removeButtons = document.querySelectorAll('[data-action="remove-rule"]');
|
||||
removeButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const index = parseInt(btn.getAttribute('data-index'));
|
||||
this.removeRule(index);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить новое правило
|
||||
*/
|
||||
addRule() {
|
||||
this.vAccessRules.push({
|
||||
type: 'Disable',
|
||||
type_file: [],
|
||||
path_access: [],
|
||||
ip_list: [],
|
||||
exceptions_dir: [],
|
||||
url_error: '404'
|
||||
});
|
||||
|
||||
this.switchTab('rules');
|
||||
this.renderRulesList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить правило
|
||||
*/
|
||||
removeRule(index) {
|
||||
this.vAccessRules.splice(index, 1);
|
||||
this.renderRulesList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Редактировать поле правила
|
||||
*/
|
||||
editRuleField(index, field) {
|
||||
const rule = this.vAccessRules[index];
|
||||
|
||||
if (field === 'type') {
|
||||
// Переключаем тип
|
||||
rule.type = rule.type === 'Allow' ? 'Disable' : 'Allow';
|
||||
this.renderRulesList();
|
||||
} else if (field === 'url_error') {
|
||||
// Простой prompt для ошибки
|
||||
const value = prompt('Страница ошибки:', rule.url_error || '404');
|
||||
if (value !== null) {
|
||||
rule.url_error = value;
|
||||
this.renderRulesList();
|
||||
}
|
||||
} else {
|
||||
// Для массивов - показываем форму редактирования
|
||||
this.showFieldEditor(index, field);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Показать редактор поля
|
||||
*/
|
||||
showFieldEditor(index, field) {
|
||||
const rule = this.vAccessRules[index];
|
||||
const fieldNames = {
|
||||
'type_file': 'Расширения файлов',
|
||||
'path_access': 'Пути доступа',
|
||||
'ip_list': 'IP адреса',
|
||||
'exceptions_dir': 'Исключения'
|
||||
};
|
||||
|
||||
const placeholders = {
|
||||
'type_file': '*.php',
|
||||
'path_access': '/admin/*',
|
||||
'ip_list': '127.0.0.1',
|
||||
'exceptions_dir': '/public/*'
|
||||
};
|
||||
|
||||
const content = `
|
||||
<div class="field-editor">
|
||||
<div class="tag-input-wrapper" style="margin-bottom: 16px;">
|
||||
<input type="text" class="form-input" id="fieldInput" placeholder="${placeholders[field]}">
|
||||
<button class="action-btn" id="addFieldValueBtn"><i class="fas fa-plus"></i> Добавить</button>
|
||||
</div>
|
||||
<div class="tags-container" id="fieldTags">
|
||||
${(rule[field] || []).map(value => `
|
||||
<span class="tag">
|
||||
${value}
|
||||
<button class="tag-remove" data-value="${value}"><i class="fas fa-times"></i></button>
|
||||
</span>
|
||||
`).join('')}
|
||||
</div>
|
||||
<button class="action-btn" id="closeFieldEditorBtn" style="margin-top: 20px;">
|
||||
<i class="fas fa-check"></i> Готово
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.editingField = { index, field };
|
||||
modal.openFieldEditor(fieldNames[field], content);
|
||||
|
||||
// Добавляем обработчики
|
||||
setTimeout(() => {
|
||||
$('addFieldValueBtn')?.addEventListener('click', () => this.addFieldValue());
|
||||
$('closeFieldEditorBtn')?.addEventListener('click', () => this.closeFieldEditor());
|
||||
|
||||
const removeButtons = document.querySelectorAll('#fieldTags .tag-remove');
|
||||
removeButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const value = btn.getAttribute('data-value');
|
||||
this.removeFieldValue(value);
|
||||
});
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить значение в поле
|
||||
*/
|
||||
addFieldValue() {
|
||||
const input = $('fieldInput');
|
||||
const value = input?.value.trim();
|
||||
|
||||
if (value && this.editingField) {
|
||||
const { index, field } = this.editingField;
|
||||
if (!this.vAccessRules[index][field]) {
|
||||
this.vAccessRules[index][field] = [];
|
||||
}
|
||||
this.vAccessRules[index][field].push(value);
|
||||
input.value = '';
|
||||
this.showFieldEditor(index, field);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить значение из поля
|
||||
*/
|
||||
removeFieldValue(value) {
|
||||
if (this.editingField) {
|
||||
const { index, field } = this.editingField;
|
||||
const arr = this.vAccessRules[index][field];
|
||||
const idx = arr.indexOf(value);
|
||||
if (idx > -1) {
|
||||
arr.splice(idx, 1);
|
||||
this.showFieldEditor(index, field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Закрыть редактор поля
|
||||
*/
|
||||
closeFieldEditor() {
|
||||
modal.closeFieldEditor();
|
||||
this.renderRulesList();
|
||||
}
|
||||
|
||||
// Drag & Drop handlers
|
||||
onDragStart(event) {
|
||||
this.draggedIndex = parseInt(event.target.getAttribute('data-index'));
|
||||
event.target.style.opacity = '0.5';
|
||||
}
|
||||
|
||||
onDragOver(event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
onDrop(event) {
|
||||
event.preventDefault();
|
||||
const dropIndex = parseInt(event.target.closest('tr').getAttribute('data-index'));
|
||||
|
||||
if (this.draggedIndex === null || this.draggedIndex === dropIndex) return;
|
||||
|
||||
const draggedRule = this.vAccessRules[this.draggedIndex];
|
||||
this.vAccessRules.splice(this.draggedIndex, 1);
|
||||
this.vAccessRules.splice(dropIndex, 0, draggedRule);
|
||||
|
||||
this.draggedIndex = null;
|
||||
this.renderRulesList();
|
||||
}
|
||||
}
|
||||
|
||||
586
Backend/admin/frontend/assets/js/main.js
Normal file
586
Backend/admin/frontend/assets/js/main.js
Normal file
@@ -0,0 +1,586 @@
|
||||
/* ============================================
|
||||
vServer Admin Panel - Main Entry Point
|
||||
Точка входа приложения
|
||||
============================================ */
|
||||
|
||||
import { log, isWailsAvailable, sleep } from './utils/helpers.js';
|
||||
import { WindowControls } from './ui/window.js';
|
||||
import { Navigation } from './ui/navigation.js';
|
||||
import { notification } from './ui/notification.js';
|
||||
import { modal } from './ui/modal.js';
|
||||
import { ServicesManager } from './components/services.js';
|
||||
import { SitesManager } from './components/sites.js';
|
||||
import { ProxyManager } from './components/proxy.js';
|
||||
import { VAccessManager } from './components/vaccess.js';
|
||||
import { configAPI } from './api/config.js';
|
||||
import { $ } from './utils/dom.js';
|
||||
|
||||
/**
|
||||
* Главный класс приложения
|
||||
*/
|
||||
class App {
|
||||
constructor() {
|
||||
this.windowControls = new WindowControls();
|
||||
this.navigation = new Navigation();
|
||||
this.servicesManager = new ServicesManager();
|
||||
this.sitesManager = new SitesManager();
|
||||
this.proxyManager = new ProxyManager();
|
||||
this.vAccessManager = new VAccessManager();
|
||||
|
||||
this.isWails = isWailsAvailable();
|
||||
|
||||
log('Приложение инициализировано');
|
||||
}
|
||||
|
||||
/**
|
||||
* Запустить приложение
|
||||
*/
|
||||
async start() {
|
||||
log('Запуск приложения...');
|
||||
|
||||
// Скрываем loader если не в Wails
|
||||
if (!this.isWails) {
|
||||
notification.hideLoader();
|
||||
}
|
||||
|
||||
// Ждём немного перед загрузкой данных
|
||||
await sleep(1000);
|
||||
|
||||
if (this.isWails) {
|
||||
log('Wails API доступен', 'info');
|
||||
} else {
|
||||
log('Wails API недоступен (браузерный режим)', 'warn');
|
||||
}
|
||||
|
||||
// Загружаем начальные данные
|
||||
await this.loadInitialData();
|
||||
|
||||
// Запускаем автообновление
|
||||
this.startAutoRefresh();
|
||||
|
||||
// Скрываем loader после загрузки
|
||||
if (this.isWails) {
|
||||
notification.hideLoader();
|
||||
}
|
||||
|
||||
// Настраиваем глобальные функции для совместимости
|
||||
this.setupGlobalHandlers();
|
||||
|
||||
// Привязываем кнопки
|
||||
this.setupButtons();
|
||||
|
||||
log('Приложение запущено');
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузить начальные данные
|
||||
*/
|
||||
async loadInitialData() {
|
||||
await Promise.all([
|
||||
this.servicesManager.loadStatus(),
|
||||
this.sitesManager.load(),
|
||||
this.proxyManager.load()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Запустить автообновление
|
||||
*/
|
||||
startAutoRefresh() {
|
||||
setInterval(async () => {
|
||||
await this.loadInitialData();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Привязать кнопки
|
||||
*/
|
||||
setupButtons() {
|
||||
// Кнопка сохранения настроек
|
||||
const saveSettingsBtn = $('saveSettingsBtn');
|
||||
if (saveSettingsBtn) {
|
||||
saveSettingsBtn.addEventListener('click', async () => {
|
||||
await this.saveConfigSettings();
|
||||
});
|
||||
}
|
||||
|
||||
// Кнопка сохранения vAccess (добавляем обработчик динамически при открытии vAccess)
|
||||
// Обработчик будет добавлен в VAccessManager.open()
|
||||
|
||||
// Моментальное переключение Proxy без перезапуска
|
||||
const proxyCheckbox = $('proxyEnabled');
|
||||
if (proxyCheckbox) {
|
||||
proxyCheckbox.addEventListener('change', async (e) => {
|
||||
const isEnabled = e.target.checked;
|
||||
|
||||
if (isEnabled) {
|
||||
await configAPI.enableProxyService();
|
||||
notification.success('Proxy Manager включен', 1000);
|
||||
} else {
|
||||
await configAPI.disableProxyService();
|
||||
notification.success('Proxy Manager отключен', 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Настроить глобальные обработчики
|
||||
*/
|
||||
setupGlobalHandlers() {
|
||||
// Для vAccess
|
||||
window.editVAccess = (host, isProxy) => {
|
||||
this.vAccessManager.open(host, isProxy);
|
||||
};
|
||||
|
||||
window.backToMain = () => {
|
||||
this.vAccessManager.backToMain();
|
||||
};
|
||||
|
||||
window.switchVAccessTab = (tab) => {
|
||||
this.vAccessManager.switchTab(tab);
|
||||
};
|
||||
|
||||
window.saveVAccessChanges = async () => {
|
||||
await this.vAccessManager.save();
|
||||
};
|
||||
|
||||
window.addVAccessRule = () => {
|
||||
this.vAccessManager.addRule();
|
||||
};
|
||||
|
||||
// Для Settings
|
||||
window.loadConfig = async () => {
|
||||
await this.loadConfigSettings();
|
||||
};
|
||||
|
||||
window.saveSettings = async () => {
|
||||
await this.saveConfigSettings();
|
||||
};
|
||||
|
||||
// Для модальных окон
|
||||
window.editSite = (index) => {
|
||||
this.editSite(index);
|
||||
};
|
||||
|
||||
window.editProxy = (index) => {
|
||||
this.editProxy(index);
|
||||
};
|
||||
|
||||
window.setStatus = (status) => {
|
||||
this.setModalStatus(status);
|
||||
};
|
||||
|
||||
window.setProxyStatus = (status) => {
|
||||
this.setModalStatus(status);
|
||||
};
|
||||
|
||||
window.addAliasTag = () => {
|
||||
this.addAliasTag();
|
||||
};
|
||||
|
||||
window.removeAliasTag = (btn) => {
|
||||
btn.parentElement.remove();
|
||||
};
|
||||
|
||||
window.saveModalData = async () => {
|
||||
await this.saveModalData();
|
||||
};
|
||||
|
||||
// Drag & Drop для vAccess
|
||||
window.dragStart = (event, index) => {
|
||||
this.vAccessManager.onDragStart(event);
|
||||
};
|
||||
|
||||
window.dragOver = (event) => {
|
||||
this.vAccessManager.onDragOver(event);
|
||||
};
|
||||
|
||||
window.drop = (event, index) => {
|
||||
this.vAccessManager.onDrop(event);
|
||||
};
|
||||
|
||||
window.editRuleField = (index, field) => {
|
||||
this.vAccessManager.editRuleField(index, field);
|
||||
};
|
||||
|
||||
window.removeVAccessRule = (index) => {
|
||||
this.vAccessManager.removeRule(index);
|
||||
};
|
||||
|
||||
window.closeFieldEditor = () => {
|
||||
this.vAccessManager.closeFieldEditor();
|
||||
};
|
||||
|
||||
window.addFieldValue = () => {
|
||||
this.vAccessManager.addFieldValue();
|
||||
};
|
||||
|
||||
window.removeFieldValue = (value) => {
|
||||
this.vAccessManager.removeFieldValue(value);
|
||||
};
|
||||
|
||||
// Тестовые функции (для браузерного режима)
|
||||
window.editTestSite = (index) => {
|
||||
const testSites = [
|
||||
{name: 'Локальный сайт', host: '127.0.0.1', alias: ['localhost'], status: 'active', root_file: 'index.html', root_file_routing: true},
|
||||
{name: 'Тестовый проект', host: 'test.local', alias: ['*.test.local', 'test.com'], status: 'active', root_file: 'index.php', root_file_routing: false},
|
||||
{name: 'API сервис', host: 'api.example.com', alias: ['*.api.example.com'], status: 'inactive', root_file: 'index.php', root_file_routing: true}
|
||||
];
|
||||
this.sitesManager.sitesData = testSites;
|
||||
this.editSite(index);
|
||||
};
|
||||
|
||||
window.editTestProxy = (index) => {
|
||||
const testProxies = [
|
||||
{enable: true, external_domain: 'git.example.ru', local_address: '127.0.0.1', local_port: '3333', service_https_use: false, auto_https: true},
|
||||
{enable: true, external_domain: 'api.example.com', local_address: '127.0.0.1', local_port: '8080', service_https_use: true, auto_https: false},
|
||||
{enable: false, external_domain: 'test.example.net', local_address: '127.0.0.1', local_port: '5000', service_https_use: false, auto_https: false}
|
||||
];
|
||||
this.proxyManager.proxiesData = testProxies;
|
||||
this.editProxy(index);
|
||||
};
|
||||
|
||||
window.openTestLink = (url) => {
|
||||
this.sitesManager.openLink(url);
|
||||
};
|
||||
|
||||
window.openSiteFolder = async (host) => {
|
||||
await this.sitesManager.handleAction('open-folder', { getAttribute: () => host });
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузить настройки конфигурации
|
||||
*/
|
||||
async loadConfigSettings() {
|
||||
if (!isWailsAvailable()) {
|
||||
// Тестовые данные для браузерного режима
|
||||
$('mysqlHost').value = '127.0.0.1';
|
||||
$('mysqlPort').value = 3306;
|
||||
$('phpHost').value = 'localhost';
|
||||
$('phpPort').value = 8000;
|
||||
$('proxyEnabled').checked = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await configAPI.getConfig();
|
||||
if (!config) return;
|
||||
|
||||
$('mysqlHost').value = config.Soft_Settings?.mysql_host || '127.0.0.1';
|
||||
$('mysqlPort').value = config.Soft_Settings?.mysql_port || 3306;
|
||||
$('phpHost').value = config.Soft_Settings?.php_host || 'localhost';
|
||||
$('phpPort').value = config.Soft_Settings?.php_port || 8000;
|
||||
$('proxyEnabled').checked = config.Soft_Settings?.proxy_enabled !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить настройки конфигурации
|
||||
*/
|
||||
async saveConfigSettings() {
|
||||
const saveBtn = $('saveSettingsBtn');
|
||||
const originalText = saveBtn.querySelector('span').textContent;
|
||||
|
||||
if (!isWailsAvailable()) {
|
||||
notification.success('Настройки сохранены (тестовый режим)', 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.querySelector('span').textContent = 'Сохранение...';
|
||||
|
||||
const config = await configAPI.getConfig();
|
||||
config.Soft_Settings.mysql_host = $('mysqlHost').value;
|
||||
config.Soft_Settings.mysql_port = parseInt($('mysqlPort').value);
|
||||
config.Soft_Settings.php_host = $('phpHost').value;
|
||||
config.Soft_Settings.php_port = parseInt($('phpPort').value);
|
||||
config.Soft_Settings.proxy_enabled = $('proxyEnabled').checked;
|
||||
|
||||
const configJSON = JSON.stringify(config, null, 4);
|
||||
const result = await configAPI.saveConfig(configJSON);
|
||||
|
||||
if (result.startsWith('Error')) {
|
||||
notification.error(result);
|
||||
return;
|
||||
}
|
||||
|
||||
saveBtn.querySelector('span').textContent = 'Перезапуск сервисов...';
|
||||
await configAPI.restartAllServices();
|
||||
|
||||
notification.success('Настройки сохранены и сервисы перезапущены!', 1500);
|
||||
} catch (error) {
|
||||
notification.error('Ошибка: ' + error.message);
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.querySelector('span').textContent = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Редактировать сайт
|
||||
*/
|
||||
editSite(index) {
|
||||
const site = this.sitesManager.sitesData[index];
|
||||
if (!site) return;
|
||||
|
||||
const content = `
|
||||
<div class="settings-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Статус сайта:</label>
|
||||
<div class="status-toggle">
|
||||
<button class="status-btn ${site.status === 'active' ? 'active' : ''}" onclick="setStatus('active')" data-value="active">
|
||||
<i class="fas fa-check-circle"></i> Active
|
||||
</button>
|
||||
<button class="status-btn ${site.status === 'inactive' ? 'active' : ''}" onclick="setStatus('inactive')" data-value="inactive">
|
||||
<i class="fas fa-times-circle"></i> Inactive
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Название сайта:</label>
|
||||
<input type="text" class="form-input" id="editName" value="${site.name}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Host:</label>
|
||||
<input type="text" class="form-input" id="editHost" value="${site.host}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Alias:</label>
|
||||
<div class="tag-input-wrapper">
|
||||
<input type="text" class="form-input" id="editAliasInput" placeholder="Введите alias и нажмите Добавить...">
|
||||
<button class="action-btn" onclick="addAliasTag()"><i class="fas fa-plus"></i> Добавить</button>
|
||||
</div>
|
||||
<div class="tags-container" id="aliasTagsContainer">
|
||||
${site.alias.map(alias => `
|
||||
<span class="tag">
|
||||
${alias}
|
||||
<button class="tag-remove" onclick="removeAliasTag(this)"><i class="fas fa-times"></i></button>
|
||||
</span>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Root файл:</label>
|
||||
<input type="text" class="form-input" id="editRootFile" value="${site.root_file}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Роутинг:</label>
|
||||
<div class="toggle-wrapper">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="editRouting" ${site.root_file_routing ? 'checked' : ''}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
<span class="toggle-label">Включён</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.open('Редактировать сайт', content);
|
||||
window.currentEditType = 'site';
|
||||
window.currentEditIndex = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Редактировать прокси
|
||||
*/
|
||||
editProxy(index) {
|
||||
const proxy = this.proxyManager.proxiesData[index];
|
||||
if (!proxy) return;
|
||||
|
||||
const content = `
|
||||
<div class="settings-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Статус прокси:</label>
|
||||
<div class="status-toggle">
|
||||
<button class="status-btn ${proxy.enable ? 'active' : ''}" onclick="setProxyStatus('enable')" data-value="enable">
|
||||
<i class="fas fa-check-circle"></i> Включён
|
||||
</button>
|
||||
<button class="status-btn ${!proxy.enable ? 'active' : ''}" onclick="setProxyStatus('disable')" data-value="disable">
|
||||
<i class="fas fa-times-circle"></i> Отключён
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Внешний домен:</label>
|
||||
<input type="text" class="form-input" id="editDomain" value="${proxy.external_domain}">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Локальный адрес:</label>
|
||||
<input type="text" class="form-input" id="editLocalAddr" value="${proxy.local_address}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Локальный порт:</label>
|
||||
<input type="text" class="form-input" id="editLocalPort" value="${proxy.local_port}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">HTTPS к сервису:</label>
|
||||
<div class="toggle-wrapper">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="editServiceHTTPS" ${proxy.service_https_use ? 'checked' : ''}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
<span class="toggle-label">Включён</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Авто HTTPS:</label>
|
||||
<div class="toggle-wrapper">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="editAutoHTTPS" ${proxy.auto_https ? 'checked' : ''}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
<span class="toggle-label">Включён</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.open('Редактировать прокси', content);
|
||||
window.currentEditType = 'proxy';
|
||||
window.currentEditIndex = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Установить статус в модальном окне
|
||||
*/
|
||||
setModalStatus(status) {
|
||||
const buttons = document.querySelectorAll('.status-btn');
|
||||
buttons.forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
if (btn.dataset.value === status) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить alias tag
|
||||
*/
|
||||
addAliasTag() {
|
||||
const input = $('editAliasInput');
|
||||
const value = input?.value.trim();
|
||||
|
||||
if (value) {
|
||||
const container = $('aliasTagsContainer');
|
||||
const tag = document.createElement('span');
|
||||
tag.className = 'tag';
|
||||
tag.innerHTML = `
|
||||
${value}
|
||||
<button class="tag-remove" onclick="removeAliasTag(this)"><i class="fas fa-times"></i></button>
|
||||
`;
|
||||
container.appendChild(tag);
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить данные модального окна
|
||||
*/
|
||||
async saveModalData() {
|
||||
if (!isWailsAvailable()) {
|
||||
notification.success('Данные сохранены (тестовый режим)', 1000);
|
||||
modal.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.currentEditType === 'site') {
|
||||
await this.saveSiteData();
|
||||
} else if (window.currentEditType === 'proxy') {
|
||||
await this.saveProxyData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить данные сайта
|
||||
*/
|
||||
async saveSiteData() {
|
||||
const index = window.currentEditIndex;
|
||||
const tags = document.querySelectorAll('#aliasTagsContainer .tag');
|
||||
const aliases = Array.from(tags).map(tag => tag.textContent.trim());
|
||||
const statusBtn = document.querySelector('.status-btn.active');
|
||||
|
||||
const config = await configAPI.getConfig();
|
||||
config.Site_www[index] = {
|
||||
name: $('editName').value,
|
||||
host: $('editHost').value,
|
||||
alias: aliases,
|
||||
status: statusBtn ? statusBtn.dataset.value : 'active',
|
||||
root_file: $('editRootFile').value,
|
||||
root_file_routing: $('editRouting').checked
|
||||
};
|
||||
|
||||
const configJSON = JSON.stringify(config, null, 4);
|
||||
const result = await configAPI.saveConfig(configJSON);
|
||||
|
||||
if (result.startsWith('Error')) {
|
||||
notification.error(result);
|
||||
} else {
|
||||
notification.show('Перезапуск HTTP/HTTPS...', 'success', 800);
|
||||
|
||||
await configAPI.stopHTTPService();
|
||||
await configAPI.stopHTTPSService();
|
||||
await sleep(500);
|
||||
await configAPI.startHTTPService();
|
||||
await configAPI.startHTTPSService();
|
||||
|
||||
notification.success('Изменения сохранены и применены!', 1000);
|
||||
await this.sitesManager.load();
|
||||
modal.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить данные прокси
|
||||
*/
|
||||
async saveProxyData() {
|
||||
const index = window.currentEditIndex;
|
||||
const statusBtn = document.querySelector('.status-btn.active');
|
||||
const isEnabled = statusBtn && statusBtn.dataset.value === 'enable';
|
||||
|
||||
const config = await configAPI.getConfig();
|
||||
config.Proxy_Service[index] = {
|
||||
Enable: isEnabled,
|
||||
ExternalDomain: $('editDomain').value,
|
||||
LocalAddress: $('editLocalAddr').value,
|
||||
LocalPort: $('editLocalPort').value,
|
||||
ServiceHTTPSuse: $('editServiceHTTPS').checked,
|
||||
AutoHTTPS: $('editAutoHTTPS').checked
|
||||
};
|
||||
|
||||
const configJSON = JSON.stringify(config, null, 4);
|
||||
const result = await configAPI.saveConfig(configJSON);
|
||||
|
||||
if (result.startsWith('Error')) {
|
||||
notification.error(result);
|
||||
} else {
|
||||
notification.show('Перезапуск HTTP/HTTPS...', 'success', 800);
|
||||
|
||||
await configAPI.stopHTTPService();
|
||||
await configAPI.stopHTTPSService();
|
||||
await sleep(500);
|
||||
await configAPI.startHTTPService();
|
||||
await configAPI.startHTTPSService();
|
||||
|
||||
notification.success('Изменения сохранены и применены!', 1000);
|
||||
await this.proxyManager.load();
|
||||
modal.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация приложения при загрузке DOM
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const app = new App();
|
||||
app.start();
|
||||
});
|
||||
|
||||
log('vServer Admin Panel загружен');
|
||||
|
||||
101
Backend/admin/frontend/assets/js/ui/modal.js
Normal file
101
Backend/admin/frontend/assets/js/ui/modal.js
Normal file
@@ -0,0 +1,101 @@
|
||||
/* ============================================
|
||||
Modal Manager
|
||||
Управление модальными окнами
|
||||
============================================ */
|
||||
|
||||
import { $, addClass, removeClass } from '../utils/dom.js';
|
||||
|
||||
/**
|
||||
* Класс для управления модальными окнами
|
||||
*/
|
||||
export class Modal {
|
||||
constructor() {
|
||||
this.overlay = $('modalOverlay');
|
||||
this.title = $('modalTitle');
|
||||
this.content = $('modalContent');
|
||||
this.closeBtn = $('modalCloseBtn');
|
||||
this.cancelBtn = $('modalCancelBtn');
|
||||
this.saveBtn = $('modalSaveBtn');
|
||||
this.fieldEditorOverlay = $('fieldEditorOverlay');
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.closeBtn) {
|
||||
this.closeBtn.addEventListener('click', () => this.close());
|
||||
}
|
||||
|
||||
if (this.cancelBtn) {
|
||||
this.cancelBtn.addEventListener('click', () => this.close());
|
||||
}
|
||||
|
||||
if (this.saveBtn) {
|
||||
this.saveBtn.addEventListener('click', () => {
|
||||
if (window.saveModalData) {
|
||||
window.saveModalData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.overlay) {
|
||||
this.overlay.addEventListener('click', (e) => {
|
||||
if (e.target === this.overlay) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Открыть модальное окно
|
||||
* @param {string} title - Заголовок
|
||||
* @param {string} htmlContent - HTML контент
|
||||
*/
|
||||
open(title, htmlContent) {
|
||||
if (this.title) this.title.textContent = title;
|
||||
if (this.content) this.content.innerHTML = htmlContent;
|
||||
if (this.overlay) addClass(this.overlay, 'show');
|
||||
}
|
||||
|
||||
/**
|
||||
* Закрыть модальное окно
|
||||
*/
|
||||
close() {
|
||||
if (this.overlay) removeClass(this.overlay, 'show');
|
||||
}
|
||||
|
||||
/**
|
||||
* Установить обработчик сохранения
|
||||
* @param {Function} callback - Функция обратного вызова
|
||||
*/
|
||||
onSave(callback) {
|
||||
if (this.saveBtn) {
|
||||
this.saveBtn.onclick = callback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Открыть редактор поля
|
||||
* @param {string} title - Заголовок
|
||||
* @param {string} htmlContent - HTML контент
|
||||
*/
|
||||
openFieldEditor(title, htmlContent) {
|
||||
const fieldTitle = $('fieldEditorTitle');
|
||||
const fieldContent = $('fieldEditorContent');
|
||||
|
||||
if (fieldTitle) fieldTitle.textContent = title;
|
||||
if (fieldContent) fieldContent.innerHTML = htmlContent;
|
||||
if (this.fieldEditorOverlay) addClass(this.fieldEditorOverlay, 'show');
|
||||
}
|
||||
|
||||
/**
|
||||
* Закрыть редактор поля
|
||||
*/
|
||||
closeFieldEditor() {
|
||||
if (this.fieldEditorOverlay) removeClass(this.fieldEditorOverlay, 'show');
|
||||
}
|
||||
}
|
||||
|
||||
// Глобальный экземпляр модального окна
|
||||
export const modal = new Modal();
|
||||
|
||||
67
Backend/admin/frontend/assets/js/ui/navigation.js
Normal file
67
Backend/admin/frontend/assets/js/ui/navigation.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/* ============================================
|
||||
Navigation
|
||||
Управление навигацией
|
||||
============================================ */
|
||||
|
||||
import { $, $$, hide, show, removeClass, addClass } from '../utils/dom.js';
|
||||
|
||||
/**
|
||||
* Класс для управления навигацией
|
||||
*/
|
||||
export class Navigation {
|
||||
constructor() {
|
||||
this.navItems = $$('.nav-item');
|
||||
this.sections = {
|
||||
services: $('sectionServices'),
|
||||
sites: $('sectionSites'),
|
||||
proxy: $('sectionProxy'),
|
||||
settings: $('sectionSettings'),
|
||||
vaccess: $('sectionVAccessEditor')
|
||||
};
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.navItems.forEach((item, index) => {
|
||||
item.addEventListener('click', () => this.navigate(index));
|
||||
});
|
||||
}
|
||||
|
||||
navigate(index) {
|
||||
// Убираем active со всех навигационных элементов
|
||||
this.navItems.forEach(nav => removeClass(nav, 'active'));
|
||||
addClass(this.navItems[index], 'active');
|
||||
|
||||
// Скрываем все секции
|
||||
this.hideAllSections();
|
||||
|
||||
// Показываем нужные секции
|
||||
if (index === 0) {
|
||||
// Главная - всё кроме настроек
|
||||
show(this.sections.services);
|
||||
show(this.sections.sites);
|
||||
show(this.sections.proxy);
|
||||
} else if (index === 3) {
|
||||
// Настройки
|
||||
show(this.sections.settings);
|
||||
// Загружаем конфигурацию при открытии
|
||||
if (window.loadConfig) {
|
||||
window.loadConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideAllSections() {
|
||||
Object.values(this.sections).forEach(section => {
|
||||
if (section) hide(section);
|
||||
});
|
||||
}
|
||||
|
||||
showDashboard() {
|
||||
this.hideAllSections();
|
||||
show(this.sections.services);
|
||||
show(this.sections.sites);
|
||||
show(this.sections.proxy);
|
||||
}
|
||||
}
|
||||
|
||||
81
Backend/admin/frontend/assets/js/ui/notification.js
Normal file
81
Backend/admin/frontend/assets/js/ui/notification.js
Normal file
@@ -0,0 +1,81 @@
|
||||
/* ============================================
|
||||
Notification System
|
||||
Система уведомлений
|
||||
============================================ */
|
||||
|
||||
import { $, addClass, removeClass } from '../utils/dom.js';
|
||||
|
||||
/**
|
||||
* Класс для управления уведомлениями
|
||||
*/
|
||||
export class NotificationManager {
|
||||
constructor() {
|
||||
this.container = $('notification');
|
||||
this.loader = $('appLoader');
|
||||
}
|
||||
|
||||
/**
|
||||
* Показать уведомление
|
||||
* @param {string} message - Текст сообщения
|
||||
* @param {string} type - Тип (success, error)
|
||||
* @param {number} duration - Длительность показа (мс)
|
||||
*/
|
||||
show(message, type = 'success', duration = 1000) {
|
||||
if (!this.container) return;
|
||||
|
||||
const icon = type === 'success'
|
||||
? '<i class="fas fa-check-circle"></i>'
|
||||
: '<i class="fas fa-exclamation-circle"></i>';
|
||||
|
||||
this.container.innerHTML = `
|
||||
<div class="notification-content">
|
||||
<div class="notification-icon">${icon}</div>
|
||||
<div class="notification-text">${message}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.container.className = `notification show ${type}`;
|
||||
|
||||
setTimeout(() => {
|
||||
removeClass(this.container, 'show');
|
||||
}, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Показать успешное уведомление
|
||||
* @param {string} message - Текст сообщения
|
||||
* @param {number} duration - Длительность
|
||||
*/
|
||||
success(message, duration = 1000) {
|
||||
this.show(message, 'success', duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Показать уведомление об ошибке
|
||||
* @param {string} message - Текст сообщения
|
||||
* @param {number} duration - Длительность
|
||||
*/
|
||||
error(message, duration = 2000) {
|
||||
this.show(message, 'error', duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Скрыть загрузчик приложения
|
||||
*/
|
||||
hideLoader() {
|
||||
if (!this.loader) return;
|
||||
|
||||
setTimeout(() => {
|
||||
addClass(this.loader, 'hide');
|
||||
setTimeout(() => {
|
||||
if (this.loader.parentNode) {
|
||||
this.loader.remove();
|
||||
}
|
||||
}, 500);
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
// Глобальный экземпляр менеджера уведомлений
|
||||
export const notification = new NotificationManager();
|
||||
|
||||
51
Backend/admin/frontend/assets/js/ui/window.js
Normal file
51
Backend/admin/frontend/assets/js/ui/window.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/* ============================================
|
||||
Window Controls
|
||||
Управление окном приложения
|
||||
============================================ */
|
||||
|
||||
import { $, addClass } from '../utils/dom.js';
|
||||
|
||||
/**
|
||||
* Класс для управления окном
|
||||
*/
|
||||
export class WindowControls {
|
||||
constructor() {
|
||||
this.minimizeBtn = $('minimizeBtn');
|
||||
this.maximizeBtn = $('maximizeBtn');
|
||||
this.closeBtn = $('closeBtn');
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.minimizeBtn) {
|
||||
this.minimizeBtn.addEventListener('click', () => this.minimize());
|
||||
}
|
||||
|
||||
if (this.maximizeBtn) {
|
||||
this.maximizeBtn.addEventListener('click', () => this.maximize());
|
||||
}
|
||||
|
||||
if (this.closeBtn) {
|
||||
this.closeBtn.addEventListener('click', () => this.close());
|
||||
}
|
||||
}
|
||||
|
||||
minimize() {
|
||||
if (window.runtime?.WindowMinimise) {
|
||||
window.runtime.WindowMinimise();
|
||||
}
|
||||
}
|
||||
|
||||
maximize() {
|
||||
if (window.runtime?.WindowToggleMaximise) {
|
||||
window.runtime.WindowToggleMaximise();
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (window.runtime?.Quit) {
|
||||
window.runtime.Quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
83
Backend/admin/frontend/assets/js/utils/dom.js
Normal file
83
Backend/admin/frontend/assets/js/utils/dom.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/* ============================================
|
||||
DOM Utilities
|
||||
Утилиты для работы с DOM
|
||||
============================================ */
|
||||
|
||||
/**
|
||||
* Получить элемент по ID
|
||||
* @param {string} id - ID элемента
|
||||
* @returns {HTMLElement|null}
|
||||
*/
|
||||
export function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить все элементы по селектору
|
||||
* @param {string} selector - CSS селектор
|
||||
* @param {HTMLElement} parent - Родительский элемент
|
||||
* @returns {NodeList}
|
||||
*/
|
||||
export function $$(selector, parent = document) {
|
||||
return parent.querySelectorAll(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Показать элемент
|
||||
* @param {HTMLElement|string} element - Элемент или ID
|
||||
*/
|
||||
export function show(element) {
|
||||
const el = typeof element === 'string' ? $(element) : element;
|
||||
if (el) el.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Скрыть элемент
|
||||
* @param {HTMLElement|string} element - Элемент или ID
|
||||
*/
|
||||
export function hide(element) {
|
||||
const el = typeof element === 'string' ? $(element) : element;
|
||||
if (el) el.style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключить видимость элемента
|
||||
* @param {HTMLElement|string} element - Элемент или ID
|
||||
*/
|
||||
export function toggle(element) {
|
||||
const el = typeof element === 'string' ? $(element) : element;
|
||||
if (el) {
|
||||
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить класс
|
||||
* @param {HTMLElement|string} element - Элемент или ID
|
||||
* @param {string} className - Имя класса
|
||||
*/
|
||||
export function addClass(element, className) {
|
||||
const el = typeof element === 'string' ? $(element) : element;
|
||||
if (el) el.classList.add(className);
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить класс
|
||||
* @param {HTMLElement|string} element - Элемент или ID
|
||||
* @param {string} className - Имя класса
|
||||
*/
|
||||
export function removeClass(element, className) {
|
||||
const el = typeof element === 'string' ? $(element) : element;
|
||||
if (el) el.classList.remove(className);
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключить класс
|
||||
* @param {HTMLElement|string} element - Элемент или ID
|
||||
* @param {string} className - Имя класса
|
||||
*/
|
||||
export function toggleClass(element, className) {
|
||||
const el = typeof element === 'string' ? $(element) : element;
|
||||
if (el) el.classList.toggle(className);
|
||||
}
|
||||
|
||||
57
Backend/admin/frontend/assets/js/utils/helpers.js
Normal file
57
Backend/admin/frontend/assets/js/utils/helpers.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/* ============================================
|
||||
Helper Utilities
|
||||
Вспомогательные функции
|
||||
============================================ */
|
||||
|
||||
/**
|
||||
* Ждёт указанное время
|
||||
* @param {number} ms - Миллисекунды
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce функция
|
||||
* @param {Function} func - Функция для debounce
|
||||
* @param {number} wait - Время задержки
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет доступность Wails API
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isWailsAvailable() {
|
||||
return typeof window.go !== 'undefined' &&
|
||||
window.go?.admin?.App !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Логирование с префиксом
|
||||
* @param {string} message - Сообщение
|
||||
* @param {string} type - Тип (log, error, warn, info)
|
||||
*/
|
||||
export function log(message, type = 'log') {
|
||||
const prefix = '🚀 vServer:';
|
||||
const styles = {
|
||||
log: '✅',
|
||||
error: '❌',
|
||||
warn: '⚠️',
|
||||
info: 'ℹ️'
|
||||
};
|
||||
console[type](`${prefix} ${styles[type]} ${message}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user