feat: New structure panel, new theme

This commit is contained in:
Konstantin Pichugin 2026-01-11 05:42:36 +03:00
parent beac0cdf67
commit fa7759280b
9 changed files with 157 additions and 12 deletions

File diff suppressed because one or more lines are too long

View file

@ -7,7 +7,8 @@
<a-layout id="content-layout"> <a-layout id="content-layout">
<a-layout-content :style="{ padding: '24px 16px' }"> <a-layout-content :style="{ padding: '24px 16px' }">
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'> <a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-if="loadingStates.fetched"> <transition name="list" appear>
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-if="loadingStates.fetched">
<a-col> <a-col>
<a-card size="small" :style="{ padding: '16px' }" hoverable> <a-card size="small" :style="{ padding: '16px' }" hoverable>
<h2>{{ i18n "pages.clients.title" }}</h2> <h2>{{ i18n "pages.clients.title" }}</h2>
@ -133,6 +134,7 @@
<a-spin tip='{{ i18n "loading" }}'></a-spin> <a-spin tip='{{ i18n "loading" }}'></a-spin>
</a-card> </a-card>
</a-row> </a-row>
</transition>
</a-spin> </a-spin>
</a-layout-content> </a-layout-content>
</a-layout> </a-layout>

View file

@ -6,7 +6,7 @@
<a-theme-switch></a-theme-switch> <a-theme-switch></a-theme-switch>
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="activeTab" <a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="activeTab"
@click="({key}) => openLink(key)"> @click="({key}) => openLink(key)">
<a-menu-item v-for="tab in tabs" :key="tab.key"> <a-menu-item v-for="tab in tabs" :key="tab.key" :data-menu-key="tab.key">
<a-icon :type="tab.icon"></a-icon> <a-icon :type="tab.icon"></a-icon>
<span v-text="tab.title"></span> <span v-text="tab.title"></span>
</a-menu-item> </a-menu-item>
@ -20,7 +20,7 @@
<a-theme-switch></a-theme-switch> <a-theme-switch></a-theme-switch>
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="activeTab" <a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="activeTab"
@click="({key}) => openLink(key)"> @click="({key}) => openLink(key)">
<a-menu-item v-for="tab in tabs" :key="tab.key"> <a-menu-item v-for="tab in tabs" :key="tab.key" :data-menu-key="tab.key">
<a-icon :type="tab.icon"></a-icon> <a-icon :type="tab.icon"></a-icon>
<span v-text="tab.title"></span> <span v-text="tab.title"></span>
</a-menu-item> </a-menu-item>

View file

@ -9,7 +9,7 @@
<a-menu-item id="change-theme" class="ant-menu-theme-switch" @mousedown="themeSwitcher.animationsOff()"> <a-menu-item id="change-theme" class="ant-menu-theme-switch" @mousedown="themeSwitcher.animationsOff()">
<span>{{ i18n "menu.dark" }}</span> <span>{{ i18n "menu.dark" }}</span>
<a-switch :style="{ marginLeft: '2px' }" size="small" :default-checked="themeSwitcher.isDarkTheme" <a-switch :style="{ marginLeft: '2px' }" size="small" :default-checked="themeSwitcher.isDarkTheme"
@change="themeSwitcher.toggleTheme()"></a-switch> :disabled="themeSwitcher.isGlassMorphism" @change="themeSwitcher.toggleTheme()"></a-switch>
</a-menu-item> </a-menu-item>
<a-menu-item id="change-theme-ultra" v-if="themeSwitcher.isDarkTheme" class="ant-menu-theme-switch" <a-menu-item id="change-theme-ultra" v-if="themeSwitcher.isDarkTheme" class="ant-menu-theme-switch"
@mousedown="themeSwitcher.animationsOffUltra()"> @mousedown="themeSwitcher.animationsOffUltra()">
@ -17,6 +17,12 @@
<a-checkbox :style="{ marginLeft: '2px' }" :checked="themeSwitcher.isUltra" <a-checkbox :style="{ marginLeft: '2px' }" :checked="themeSwitcher.isUltra"
@click="themeSwitcher.toggleUltra()"></a-checkbox> @click="themeSwitcher.toggleUltra()"></a-checkbox>
</a-menu-item> </a-menu-item>
<a-menu-item id="change-theme-glass" class="ant-menu-theme-switch"
@mousedown="themeSwitcher.animationsOffGlass()">
<span>{{ i18n "menu.glassMorphism" }}</span>
<a-switch :style="{ marginLeft: '2px' }" size="small" :default-checked="themeSwitcher.isGlassMorphism"
@change="themeSwitcher.toggleGlassMorphism()"></a-switch>
</a-menu-item>
</a-sub-menu> </a-sub-menu>
</a-menu> </a-menu>
</template> </template>
@ -26,13 +32,17 @@
<template> <template>
<a-space @mousedown="themeSwitcher.animationsOff()" id="change-theme" direction="vertical" :size="10" :style="{ width: '100%' }"> <a-space @mousedown="themeSwitcher.animationsOff()" id="change-theme" direction="vertical" :size="10" :style="{ width: '100%' }">
<a-space direction="horizontal" size="small"> <a-space direction="horizontal" size="small">
<a-switch size="small" :default-checked="themeSwitcher.isDarkTheme" @change="themeSwitcher.toggleTheme()"></a-switch> <a-switch size="small" :default-checked="themeSwitcher.isDarkTheme" :disabled="themeSwitcher.isGlassMorphism" @change="themeSwitcher.toggleTheme()"></a-switch>
<span>{{ i18n "menu.dark" }}</span> <span>{{ i18n "menu.dark" }}</span>
</a-space> </a-space>
<a-space v-if="themeSwitcher.isDarkTheme" direction="horizontal" size="small"> <a-space v-if="themeSwitcher.isDarkTheme" direction="horizontal" size="small">
<a-checkbox :checked="themeSwitcher.isUltra" @click="themeSwitcher.toggleUltra()"></a-checkbox> <a-checkbox :checked="themeSwitcher.isUltra" @click="themeSwitcher.toggleUltra()"></a-checkbox>
<span>{{ i18n "menu.ultraDark" }}</span> <span>{{ i18n "menu.ultraDark" }}</span>
</a-space> </a-space>
<a-space direction="horizontal" size="small">
<a-switch size="small" :default-checked="themeSwitcher.isGlassMorphism" @change="themeSwitcher.toggleGlassMorphism()"></a-switch>
<span>{{ i18n "menu.glassMorphism" }}</span>
</a-space>
</a-space> </a-space>
</template> </template>
{{end}} {{end}}
@ -40,10 +50,34 @@
{{define "component/aThemeSwitch"}} {{define "component/aThemeSwitch"}}
<script> <script>
function createThemeSwitcher() { function createThemeSwitcher() {
const isDarkTheme = localStorage.getItem('dark-mode') === 'true'; let isDarkTheme = localStorage.getItem('dark-mode') === 'true';
const isUltra = localStorage.getItem('isUltraDarkThemeEnabled') === 'true'; let isUltra = localStorage.getItem('isUltraDarkThemeEnabled') === 'true';
if (isUltra) { // Glass Morphism включен по умолчанию, если не установлено явно
document.documentElement.setAttribute('data-theme', 'ultra-dark'); let isGlassMorphism = localStorage.getItem('isGlassMorphismEnabled');
if (isGlassMorphism === null) {
isGlassMorphism = true; // По умолчанию включен
localStorage.setItem('isGlassMorphismEnabled', 'true');
} else {
isGlassMorphism = isGlassMorphism === 'true';
}
// Если включен Glass Morphism, отключаем темную тему
if (isGlassMorphism) {
isDarkTheme = false;
isUltra = false;
localStorage.setItem('dark-mode', 'false');
localStorage.setItem('isUltraDarkThemeEnabled', 'false');
document.documentElement.setAttribute('data-glass-morphism', 'true');
document.documentElement.removeAttribute('data-theme');
} else {
// Если включена темная тема, отключаем Glass Morphism
if (isDarkTheme) {
isGlassMorphism = false;
localStorage.setItem('isGlassMorphismEnabled', 'false');
document.documentElement.removeAttribute('data-glass-morphism');
}
if (isUltra) {
document.documentElement.setAttribute('data-theme', 'ultra-dark');
}
} }
const theme = isDarkTheme ? 'dark' : 'light'; const theme = isDarkTheme ? 'dark' : 'light';
document.querySelector('body').setAttribute('class', theme); document.querySelector('body').setAttribute('class', theme);
@ -68,13 +102,33 @@
document.documentElement.removeAttribute('data-theme-animations'); document.documentElement.removeAttribute('data-theme-animations');
}); });
}, },
animationsOffGlass() {
document.documentElement.setAttribute('data-theme-animations', 'off');
const themeAnimationsGlass = document.querySelector('#change-theme-glass');
themeAnimationsGlass.addEventListener('mouseleave', () => {
document.documentElement.removeAttribute('data-theme-animations');
});
themeAnimationsGlass.addEventListener('touchend', () => {
document.documentElement.removeAttribute('data-theme-animations');
});
},
isDarkTheme, isDarkTheme,
isUltra, isUltra,
isGlassMorphism,
get currentTheme() { get currentTheme() {
return this.isDarkTheme ? 'dark' : 'light'; return this.isDarkTheme ? 'dark' : 'light';
}, },
toggleTheme() { toggleTheme() {
if (this.isGlassMorphism) {
return; // Не позволяем включать темную тему когда включен Glass Morphism
}
this.isDarkTheme = !this.isDarkTheme; this.isDarkTheme = !this.isDarkTheme;
if (this.isDarkTheme) {
// Если включаем темную тему, отключаем Glass Morphism
this.isGlassMorphism = false;
document.documentElement.removeAttribute('data-glass-morphism');
localStorage.setItem('isGlassMorphismEnabled', 'false');
}
localStorage.setItem('dark-mode', this.isDarkTheme); localStorage.setItem('dark-mode', this.isDarkTheme);
document.querySelector('body').setAttribute('class', this.isDarkTheme ? 'dark' : 'light'); document.querySelector('body').setAttribute('class', this.isDarkTheme ? 'dark' : 'light');
document.getElementById('message').className = themeSwitcher.currentTheme; document.getElementById('message').className = themeSwitcher.currentTheme;
@ -87,6 +141,23 @@
document.documentElement.removeAttribute('data-theme'); document.documentElement.removeAttribute('data-theme');
} }
localStorage.setItem('isUltraDarkThemeEnabled', this.isUltra.toString()); localStorage.setItem('isUltraDarkThemeEnabled', this.isUltra.toString());
},
toggleGlassMorphism() {
this.isGlassMorphism = !this.isGlassMorphism;
if (this.isGlassMorphism) {
// Если включаем Glass Morphism, отключаем темную тему
this.isDarkTheme = false;
document.querySelector('body').setAttribute('class', 'light');
document.documentElement.removeAttribute('data-theme');
this.isUltra = false;
localStorage.setItem('dark-mode', 'false');
localStorage.setItem('isUltraDarkThemeEnabled', 'false');
document.documentElement.setAttribute('data-glass-morphism', 'true');
document.getElementById('message').className = 'light';
} else {
document.documentElement.removeAttribute('data-glass-morphism');
}
localStorage.setItem('isGlassMorphismEnabled', this.isGlassMorphism.toString());
} }
}; };
} }

View file

@ -6,7 +6,8 @@
<a-sidebar></a-sidebar> <a-sidebar></a-sidebar>
<a-layout id="content-layout"> <a-layout id="content-layout">
<a-layout-content :style="{ padding: '24px 16px' }"> <a-layout-content :style="{ padding: '24px 16px' }">
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-if="loadingStates.fetched && multiNodeMode"> <transition name="list" appear>
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-if="loadingStates.fetched && multiNodeMode">
<a-col> <a-col>
<a-card size="small" :style="{ padding: '16px' }" hoverable> <a-card size="small" :style="{ padding: '16px' }" hoverable>
<h2>{{ i18n "pages.hosts.title" }}</h2> <h2>{{ i18n "pages.hosts.title" }}</h2>
@ -69,6 +70,7 @@
<a-spin tip='{{ i18n "loading" }}'></a-spin> <a-spin tip='{{ i18n "loading" }}'></a-spin>
</a-card> </a-card>
</a-row> </a-row>
</transition>
</a-layout-content> </a-layout-content>
</a-layout> </a-layout>
</a-layout> </a-layout>

View file

@ -123,6 +123,8 @@
this.loadingStates.spinning = true; this.loadingStates.spinning = true;
const msg = await HttpUtil.post('/login', this.user); const msg = await HttpUtil.post('/login', this.user);
if (msg.success) { if (msg.success) {
// Устанавливаем флаг для показа popup "Что нового?" после логина
sessionStorage.setItem('showWhatsNew', 'true');
location.href = basePath + 'panel/'; location.href = basePath + 'panel/';
} }
this.loadingStates.spinning = false; this.loadingStates.spinning = false;

View file

@ -6,7 +6,8 @@
<a-sidebar></a-sidebar> <a-sidebar></a-sidebar>
<a-layout id="content-layout"> <a-layout id="content-layout">
<a-layout-content :style="{ padding: '24px 16px' }"> <a-layout-content :style="{ padding: '24px 16px' }">
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-if="loadingStates.fetched"> <transition name="list" appear>
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-if="loadingStates.fetched">
<a-col> <a-col>
<a-card size="small" :style="{ padding: '16px' }" hoverable> <a-card size="small" :style="{ padding: '16px' }" hoverable>
<h2>{{ i18n "pages.nodes.title" }}</h2> <h2>{{ i18n "pages.nodes.title" }}</h2>
@ -95,6 +96,7 @@
<a-spin tip='{{ i18n "loading" }}'></a-spin> <a-spin tip='{{ i18n "loading" }}'></a-spin>
</a-card> </a-card>
</a-row> </a-row>
</transition>
</a-layout-content> </a-layout-content>
</a-layout> </a-layout>
</a-layout> </a-layout>

View file

@ -740,6 +740,7 @@
"theme" = "Theme" "theme" = "Theme"
"dark" = "Dark" "dark" = "Dark"
"ultraDark" = "Ultra Dark" "ultraDark" = "Ultra Dark"
"glassMorphism" = "Glass Morphism"
"dashboard" = "Overview" "dashboard" = "Overview"
"inbounds" = "Inbounds" "inbounds" = "Inbounds"
"clients" = "Clients" "clients" = "Clients"
@ -749,6 +750,38 @@
"hosts" = "Hosts" "hosts" = "Hosts"
"logout" = "Log Out" "logout" = "Log Out"
"link" = "Manage" "link" = "Manage"
"tutorial" = "Tutorial"
"restartTutorial" = "Restart Tutorial"
[tutorial]
"title" = "Web Panel Menu Guide"
"next" = "Next"
"prev" = "Previous"
"skip" = "Skip"
"finish" = "Finish"
"step" = "Step"
"of" = "of"
"dashboardTitle" = "1. Panel"
"dashboardDesc" = "Main interface for managing the entire system. Through the panel we:\n\n• Configure and control nodes (servers with xray)\n• Manage clients\n• Configure inbounds"
"dashboardHint" = "Panel is the control center. Here you create nodes, inbounds and assign clients."
"nodeTitle" = "2. Node"
"nodeDesc" = "Separate Xray core with API for communication with the panel. The node handles connections and serves as the 'brain' for inbounds."
"nodeHint" = "Node is the server core. The panel communicates with it via API to manage configs and connections."
"inboundTitle" = "3. Inbound"
"inboundDesc" = "Configuration or profile for a node.\n\n• Creates a connection to a node\n• Subscribes to one or more nodes"
"inboundHint" = "Inbound is a connection profile. Through it, clients get access to the required nodes."
"clientTitle" = "4. Client"
"clientDesc" = "System user who can be assigned one or more inbounds.\n\nFor example:\n• Inbound 1 → whitelist node\n• Inbound 2 → regular foreign server\n\nClient can use any or all inbounds assigned to them"
"clientHint" = "Client is a user. Assign inbounds to them so they can connect to the required nodes."
"hostTitle" = "5. Hosts"
"hostDesc" = "External addresses for connection.\n\n• Proxy balancer that hides direct node addresses\n• Can distribute load between multiple nodes\n• Replace node address with the required host in inbound\n\nExample:\nInbound connects to a balancer host, which then distributes the connection to real nodes"
"hostHint" = "Hosts are virtual addresses. Use them for load balancing and hiding real servers."
"settingsTitle" = "6. Panel Settings"
"settingsDesc" = "Section for general panel configuration.\n\n• Enable/disable various features\n• Configure panel appearance and behavior"
"settingsHint" = "Panel Settings — here you can manage features and panel configuration."
"xrayTitle" = "7. Xray Configuration"
"xrayDesc" = "Section for fine-tuning Xray core.\n\n• Routing\n• Connection parameters and traffic routing\n• Additional advanced node configs"
"xrayHint" = "Xray Configuration — for advanced core configuration, routing management and other node parameters."
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "The parameters have been changed." "modifySettings" = "The parameters have been changed."

View file

@ -740,6 +740,7 @@
"theme" = "Тема" "theme" = "Тема"
"dark" = "Темная" "dark" = "Темная"
"ultraDark" = "Очень темная" "ultraDark" = "Очень темная"
"glassMorphism" = "Glass Morphism"
"dashboard" = "Обзор" "dashboard" = "Обзор"
"inbounds" = "Подключения" "inbounds" = "Подключения"
"clients" = "Клиенты" "clients" = "Клиенты"
@ -749,6 +750,38 @@
"hosts" = "Хосты" "hosts" = "Хосты"
"logout" = "Выйти" "logout" = "Выйти"
"link" = "Управление" "link" = "Управление"
"tutorial" = "Обучалка"
"restartTutorial" = "Повторить обучение"
[tutorial]
"title" = "Инструкция по меню веб-панели"
"next" = "Далее"
"prev" = "Назад"
"skip" = "Пропустить"
"finish" = "Завершить"
"step" = "Шаг"
"of" = "из"
"dashboardTitle" = "1. Панель"
"dashboardDesc" = "Главный интерфейс для управления всей системой. Через панель мы:\n\n• Настраиваем и контролируем ноды (серверы с xray)\n• Управляем клиентами\n• Настраиваем инбаунды"
"dashboardHint" = "Панель — это центр управления. Здесь создаются ноды, инбаунды и назначаются клиенты."
"nodeTitle" = "2. Нода"
"nodeDesc" = "Отдельное ядро Xray с API для связи с панелью. Нода выполняет работу по обработке подключений и служит «мозгом» для инбаундов."
"nodeHint" = "Нода — это серверное ядро. Панель взаимодействует с ним через API, чтобы управлять конфигами и подключениями."
"inboundTitle" = "3. Инбаунд"
"inboundDesc" = "Конфигурация или профиль для ноды.\n\n• Создаётся подключение к ноде\n• Подписывается на одну или несколько нод"
"inboundHint" = "Инбаунд — это профиль подключения. Через него клиенты получают доступ к нужным нодам."
"clientTitle" = "4. Клиент"
"clientDesc" = "Пользователь системы, которому можно назначать один или несколько инбаундов.\n\nНапример:\n• Инбаунд 1 → нода из белого списка\n• Инбаунд 2 → обычный забугорный сервер\n\nКлиент может использовать любой или все инбаунды, которые ему назначены"
"clientHint" = "Клиент — это пользователь. Назначайте ему инбаунды, чтобы он мог подключаться к нужным нодам."
"hostTitle" = "5. Хосты"
"hostDesc" = "Внешние адреса для подключения.\n\n• Прокси-балансир, скрывающий прямые адреса нод\n• Можно распределять нагрузку между несколькими нодами\n• Подменяем адрес ноды на нужный хост в инбаунде\n\nПример:\nИнбаунд подключается к хосту-балансиру, а тот уже распределяет подключение на реальные ноды"
"hostHint" = "Хосты — это виртуальные адреса. Используйте их для балансировки нагрузки и скрытия реальных серверов."
"settingsTitle" = "6. Настройки панели"
"settingsDesc" = "Раздел для общей конфигурации панели.\n\n• Включение/отключение различных функций\n• Настройка внешнего вида и поведения панели"
"settingsHint" = "Настройки панели — здесь вы можете управлять функциями и конфигурацией самой панели."
"xrayTitle" = "7. Конфигурация Xray"
"xrayDesc" = "Раздел для тонкой настройки ядра Xray.\n\n• Роутинг\n• Параметры соединений и маршрутизации трафика\n• Дополнительные продвинутые конфиги ноды"
"xrayHint" = "Конфигурация Xray — для продвинутой конфигурации ядра, управления роутингом и другими параметрами ноды."
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "Настройки изменены" "modifySettings" = "Настройки изменены"