feat: Update Easy Switch functionality to improve server and gateway management, including enhanced grouping by servers, refined UI components, and updated data structure for better user experience.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m59s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m59s
This commit is contained in:
+80
-26
@@ -2,20 +2,23 @@
|
|||||||
|
|
||||||
## Описание
|
## Описание
|
||||||
|
|
||||||
Easy Switch - это новая страница для быстрого переключения шлюзов (gateways) на основе communities из фильтров. Интерфейс вдохновлен мобильными VPN приложениями и обеспечивает удобный способ управления маршрутизацией.
|
Easy Switch - это новая страница для быстрого переключения шлюзов (gateways) внутри серверов на основе communities из фильтров. Интерфейс вдохновлен мобильными VPN приложениями и обеспечивает удобный способ управления маршрутизацией.
|
||||||
|
|
||||||
## Особенности
|
## Особенности
|
||||||
|
|
||||||
### 1. **Визуальный интерфейс**
|
### 1. **Визуальный интерфейс**
|
||||||
- Карточки серверов с флагами стран
|
- Компактные карточки шлюзов (уменьшенный размер)
|
||||||
|
- Карточки с флагами стран
|
||||||
- Отображение пинга и протокола подключения
|
- Отображение пинга и протокола подключения
|
||||||
- Метка "Fastest" для самого быстрого сервера в группе
|
- Метка "Fast" для самого быстрого шлюза в группе
|
||||||
- Активный сервер выделен синим цветом
|
- Активный шлюз выделен синим цветом
|
||||||
|
|
||||||
### 2. **Группировка по Communities**
|
### 2. **Группировка по Серверам**
|
||||||
- Все шлюзы сгруппированы по BGP communities
|
- Сервера группируются в отдельные блоки
|
||||||
- Для каждого community можно выбрать активный шлюз
|
- Каждый сервер показывает свои communities
|
||||||
- Отображается количество доступных шлюзов
|
- Для каждого community на сервере можно выбрать активный шлюз **этого сервера**
|
||||||
|
- Переключение происходит между шлюзами **внутри одного сервера**
|
||||||
|
- Отображается количество communities на каждом сервере
|
||||||
|
|
||||||
### 3. **Поиск и фильтрация**
|
### 3. **Поиск и фильтрация**
|
||||||
- Поиск по community, описанию или имени шлюза
|
- Поиск по community, описанию или имени шлюза
|
||||||
@@ -53,6 +56,42 @@ Easy Switch - это новая страница для быстрого пер
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Структура данных в приложении
|
||||||
|
|
||||||
|
После загрузки данные организованы следующим образом:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
servers = [
|
||||||
|
{
|
||||||
|
id: "server-001",
|
||||||
|
name: "SWE-HIPHOST",
|
||||||
|
description: "Шведский сервер",
|
||||||
|
communities: [
|
||||||
|
{
|
||||||
|
community: "65001:100",
|
||||||
|
description: "Стриминг сервисы",
|
||||||
|
gateways: [
|
||||||
|
{ name: "gateway1" },
|
||||||
|
{ name: "gateway2" },
|
||||||
|
{ name: "gateway3" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// ... другие communities
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// ... другие серверы
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Активные шлюзы хранятся в формате:
|
||||||
|
```javascript
|
||||||
|
activeGateways = {
|
||||||
|
"server-001:65001:100": "gateway1",
|
||||||
|
"server-002:65001:100": "gateway4",
|
||||||
|
// ключ: "{serverId}:{community}", значение: "{gatewayName}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Интерфейс
|
## Интерфейс
|
||||||
|
|
||||||
### Заголовок
|
### Заголовок
|
||||||
@@ -69,20 +108,30 @@ Easy Switch - это новая страница для быстрого пер
|
|||||||
- Поле поиска - поиск по ключевым словам
|
- Поле поиска - поиск по ключевым словам
|
||||||
- Кнопки инструментов (настройки, быстрые действия)
|
- Кнопки инструментов (настройки, быстрые действия)
|
||||||
|
|
||||||
### Карточки серверов
|
### Блоки серверов
|
||||||
|
|
||||||
Каждая карточка содержит:
|
Каждый сервер отображается в отдельной карточке с:
|
||||||
- 🏴 **Флаг страны** (если доступен)
|
- 🏴 **Флаг страны** сервера
|
||||||
- ⚡ **Метка Fastest** (для самого быстрого сервера)
|
- 🖥️ **Иконка сервера**
|
||||||
- **Название** шлюза или страны
|
- **Название** сервера
|
||||||
- **Протокол** подключения (vless/xudp и т.д.)
|
- **Описание** сервера
|
||||||
|
- **Количество communities** на этом сервере
|
||||||
|
|
||||||
|
### Карточки шлюзов
|
||||||
|
|
||||||
|
Каждая карточка шлюза (компактная) содержит:
|
||||||
|
- ⚡ **Метка Fast** (для первого шлюза в списке)
|
||||||
|
- **Название** шлюза
|
||||||
|
- **Протокол** подключения (vless и т.д.)
|
||||||
- **Пинг** в миллисекундах
|
- **Пинг** в миллисекундах
|
||||||
- ✓ **Галочка** для активного сервера
|
- ✓ **Галочка** для активного шлюза
|
||||||
|
|
||||||
### Состояния карточки
|
Карточки имеют меньший размер (col-6 col-sm-4 col-md-3 col-lg-2) для компактности.
|
||||||
|
|
||||||
|
### Состояния карточки шлюза
|
||||||
- **Неактивная** - белый фон, серая рамка
|
- **Неактивная** - белый фон, серая рамка
|
||||||
- **Активная** - синий фон, белый текст, тень
|
- **Активная** - синий фон, белый текст, тень
|
||||||
- **Hover** - подъем карточки, легкая тень
|
- **Hover** - подъем карточки на 1px, легкая тень
|
||||||
|
|
||||||
## Использование
|
## Использование
|
||||||
|
|
||||||
@@ -92,16 +141,20 @@ Easy Switch - это новая страница для быстрого пер
|
|||||||
URL: `/easy-switch`
|
URL: `/easy-switch`
|
||||||
|
|
||||||
### Переключение шлюза
|
### Переключение шлюза
|
||||||
1. Найдите нужный community в списке
|
1. Найдите нужный **сервер** в списке
|
||||||
2. Нажмите на карточку желаемого шлюза
|
2. В этом сервере найдите нужный **community**
|
||||||
3. Карточка станет синей (активной)
|
3. Нажмите на карточку желаемого **шлюза этого сервера**
|
||||||
4. Нажмите "Применить изменения" для сохранения
|
4. Карточка станет синей (активной)
|
||||||
|
5. Нажмите "Применить изменения" для сохранения
|
||||||
|
|
||||||
|
**Важно:** Переключение происходит между шлюзами **внутри одного сервера**. Каждый сервер имеет свой набор шлюзов для каждого community.
|
||||||
|
|
||||||
### Поиск
|
### Поиск
|
||||||
Введите в поле поиска:
|
Введите в поле поиска:
|
||||||
|
- Название сервера (например, `SWE-HIPHOST`)
|
||||||
- Номер community (например, `65001:100`)
|
- Номер community (например, `65001:100`)
|
||||||
- Описание (например, `Стриминг`)
|
- Описание (например, `Стриминг`)
|
||||||
- Название шлюза (например, `SWE-HIPHOST`)
|
- Название шлюза (например, `gateway1`)
|
||||||
|
|
||||||
## Технические детали
|
## Технические детали
|
||||||
|
|
||||||
@@ -124,12 +177,13 @@ GET /api/servers // Метаданные серверов
|
|||||||
```javascript
|
```javascript
|
||||||
- loading: boolean // Загрузка данных
|
- loading: boolean // Загрузка данных
|
||||||
- error: string // Текст ошибки
|
- error: string // Текст ошибки
|
||||||
- servers: [] // Список серверов
|
- servers: [] // Список серверов с communities и gateways
|
||||||
- communities: [] // Группы communities
|
- inventoryServers: [] // Метаданные серверов (флаги, провайдеры)
|
||||||
- activeGateways: {} // Активные шлюзы
|
- activeGateways: {} // Активные шлюзы (ключ: "serverId:community")
|
||||||
- hasChanges: boolean // Наличие изменений
|
- hasChanges: boolean // Наличие изменений
|
||||||
- searchQuery: string // Поисковый запрос
|
- searchQuery: string // Поисковый запрос
|
||||||
- activeTab: string // Активная вкладка
|
- selectedRule: string // Выбранное правило фильтрации
|
||||||
|
- activeTab: string // Активная вкладка (proxies/providers)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|||||||
+119
-117
@@ -3,7 +3,6 @@ import api from './lib/api.js';
|
|||||||
import {
|
import {
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconChevronDown,
|
|
||||||
IconCheck,
|
IconCheck,
|
||||||
IconWorld,
|
IconWorld,
|
||||||
IconServer,
|
IconServer,
|
||||||
@@ -15,8 +14,6 @@ import {
|
|||||||
IconBolt
|
IconBolt
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
const API_URL = '/api';
|
|
||||||
|
|
||||||
// Функция для получения флага по коду страны
|
// Функция для получения флага по коду страны
|
||||||
const countryToFlag = (code) => {
|
const countryToFlag = (code) => {
|
||||||
if (!code || typeof code !== 'string') return '';
|
if (!code || typeof code !== 'string') return '';
|
||||||
@@ -26,20 +23,6 @@ const countryToFlag = (code) => {
|
|||||||
return String.fromCodePoint(...codePoints);
|
return String.fromCodePoint(...codePoints);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Парсинг community value для отображения
|
|
||||||
const parseCommunity = (value) => {
|
|
||||||
if (!value) return { asn: '', local: '', display: '' };
|
|
||||||
const parts = String(value).split(':');
|
|
||||||
if (parts.length === 2) {
|
|
||||||
return {
|
|
||||||
asn: parts[0],
|
|
||||||
local: parts[1],
|
|
||||||
display: value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { asn: '', local: '', display: value };
|
|
||||||
};
|
|
||||||
|
|
||||||
function EasySwitchManager() {
|
function EasySwitchManager() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -47,8 +30,6 @@ function EasySwitchManager() {
|
|||||||
// Серверы и фильтры
|
// Серверы и фильтры
|
||||||
const [servers, setServers] = useState([]);
|
const [servers, setServers] = useState([]);
|
||||||
const [inventoryServers, setInventoryServers] = useState([]);
|
const [inventoryServers, setInventoryServers] = useState([]);
|
||||||
const [allFilters, setAllFilters] = useState([]); // Все фильтры со всех серверов
|
|
||||||
const [communities, setCommunities] = useState([]); // Список communities для отображения
|
|
||||||
|
|
||||||
// Поиск и фильтры
|
// Поиск и фильтры
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
@@ -81,61 +62,63 @@ function EasySwitchManager() {
|
|||||||
// Загружаем список серверов из server-configs
|
// Загружаем список серверов из server-configs
|
||||||
const serversResponse = await api.get(`/server-configs`);
|
const serversResponse = await api.get(`/server-configs`);
|
||||||
const serversList = Array.isArray(serversResponse.data) ? serversResponse.data : [];
|
const serversList = Array.isArray(serversResponse.data) ? serversResponse.data : [];
|
||||||
setServers(serversList);
|
|
||||||
|
|
||||||
// Загружаем фильтры для каждого сервера
|
// Загружаем фильтры для каждого сервера
|
||||||
const filtersPromises = serversList.map(async (server) => {
|
const serversWithFilters = await Promise.all(
|
||||||
|
serversList.map(async (server) => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`/server-filters/${server.id}`);
|
const response = await api.get(`/server-filters/${server.id}`);
|
||||||
const filters = Array.isArray(response.data) ? response.data : [];
|
const filters = Array.isArray(response.data) ? response.data : [];
|
||||||
return filters.map(f => ({
|
|
||||||
...f,
|
|
||||||
serverId: server.id,
|
|
||||||
serverName: server.name
|
|
||||||
}));
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Error loading filters for ${server.id}:`, e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const filtersArrays = await Promise.all(filtersPromises);
|
// Группируем фильтры по community внутри сервера
|
||||||
const combinedFilters = filtersArrays.flat();
|
const communitiesMap = new Map();
|
||||||
setAllFilters(combinedFilters);
|
filters.forEach(filter => {
|
||||||
|
|
||||||
// Группируем по community
|
|
||||||
const communityMap = new Map();
|
|
||||||
combinedFilters.forEach(filter => {
|
|
||||||
const comm = filter.community;
|
const comm = filter.community;
|
||||||
if (!communityMap.has(comm)) {
|
if (!communitiesMap.has(comm)) {
|
||||||
communityMap.set(comm, {
|
communitiesMap.set(comm, {
|
||||||
community: comm,
|
community: comm,
|
||||||
description: filter.description || '',
|
description: filter.description || '',
|
||||||
gateways: []
|
gateways: []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const communityData = communityMap.get(comm);
|
const communityData = communitiesMap.get(comm);
|
||||||
// Добавляем gateway, если его еще нет
|
// Добавляем gateway, если его еще нет
|
||||||
if (!communityData.gateways.find(g => g.gateway === filter.gateway && g.serverId === filter.serverId)) {
|
if (!communityData.gateways.find(g => g.name === filter.gateway)) {
|
||||||
communityData.gateways.push({
|
communityData.gateways.push({
|
||||||
gateway: filter.gateway,
|
name: filter.gateway,
|
||||||
serverId: filter.serverId,
|
|
||||||
serverName: filter.serverName,
|
|
||||||
description: filter.description
|
description: filter.description
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const communitiesArray = Array.from(communityMap.values());
|
return {
|
||||||
setCommunities(communitiesArray);
|
...server,
|
||||||
|
communities: Array.from(communitiesMap.values())
|
||||||
// Устанавливаем активные шлюзы (первый для каждого community)
|
};
|
||||||
const initialActive = {};
|
} catch (e) {
|
||||||
communitiesArray.forEach(comm => {
|
console.error(`Error loading filters for ${server.id}:`, e);
|
||||||
if (comm.gateways.length > 0) {
|
return {
|
||||||
initialActive[comm.community] = comm.gateways[0].gateway;
|
...server,
|
||||||
|
communities: []
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Фильтруем серверы, у которых есть communities
|
||||||
|
const serversWithCommunities = serversWithFilters.filter(s => s.communities.length > 0);
|
||||||
|
setServers(serversWithCommunities);
|
||||||
|
|
||||||
|
// Устанавливаем активные шлюзы (первый gateway для каждого community на каждом сервере)
|
||||||
|
const initialActive = {};
|
||||||
|
serversWithCommunities.forEach(server => {
|
||||||
|
server.communities.forEach(comm => {
|
||||||
|
if (comm.gateways.length > 0) {
|
||||||
|
const key = `${server.id}:${comm.community}`;
|
||||||
|
initialActive[key] = comm.gateways[0].name;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
setActiveGateways(initialActive);
|
setActiveGateways(initialActive);
|
||||||
|
|
||||||
@@ -147,10 +130,11 @@ function EasySwitchManager() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGatewaySelect = (community, gateway) => {
|
const handleGatewaySelect = (serverId, community, gateway) => {
|
||||||
|
const key = `${serverId}:${community}`;
|
||||||
setActiveGateways(prev => ({
|
setActiveGateways(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[community]: gateway
|
[key]: gateway
|
||||||
}));
|
}));
|
||||||
setHasChanges(true);
|
setHasChanges(true);
|
||||||
};
|
};
|
||||||
@@ -176,23 +160,29 @@ function EasySwitchManager() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Фильтрация communities по поиску
|
// Фильтрация серверов по поиску
|
||||||
const filteredCommunities = communities.filter(comm => {
|
const filteredServers = servers.filter(server => {
|
||||||
if (!searchQuery.trim()) return true;
|
if (!searchQuery.trim()) return true;
|
||||||
|
|
||||||
const search = searchQuery.toLowerCase();
|
const search = searchQuery.toLowerCase();
|
||||||
|
const serverName = (server.name || '').toLowerCase();
|
||||||
|
const serverDesc = (server.description || '').toLowerCase();
|
||||||
|
|
||||||
|
// Поиск по имени/описанию сервера
|
||||||
|
if (serverName.includes(search) || serverDesc.includes(search)) return true;
|
||||||
|
|
||||||
|
// Поиск по communities и gateways
|
||||||
|
return server.communities.some(comm => {
|
||||||
const commValue = (comm.community || '').toLowerCase();
|
const commValue = (comm.community || '').toLowerCase();
|
||||||
const desc = (comm.description || '').toLowerCase();
|
const commDesc = (comm.description || '').toLowerCase();
|
||||||
|
|
||||||
return commValue.includes(search) ||
|
return commValue.includes(search) ||
|
||||||
desc.includes(search) ||
|
commDesc.includes(search) ||
|
||||||
comm.gateways.some(g =>
|
comm.gateways.some(g => (g.name || '').toLowerCase().includes(search));
|
||||||
(g.gateway || '').toLowerCase().includes(search) ||
|
});
|
||||||
(g.serverName || '').toLowerCase().includes(search)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading && communities.length === 0) {
|
if (loading && servers.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="container-xl">
|
<div className="container-xl">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
@@ -230,7 +220,7 @@ function EasySwitchManager() {
|
|||||||
Easy Switch
|
Easy Switch
|
||||||
</h2>
|
</h2>
|
||||||
<div className="text-muted mt-1">
|
<div className="text-muted mt-1">
|
||||||
Быстрое переключение шлюзов
|
Быстрое переключение шлюзов для {servers.length} {servers.length === 1 ? 'сервера' : 'серверов'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-auto">
|
<div className="col-auto">
|
||||||
@@ -266,7 +256,7 @@ function EasySwitchManager() {
|
|||||||
onClick={() => setActiveTab('proxies')}
|
onClick={() => setActiveTab('proxies')}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Прокси ({communities.length})
|
Прокси ({servers.reduce((sum, s) => sum + s.communities.length, 0)})
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav-item" role="presentation">
|
<li className="nav-item" role="presentation">
|
||||||
@@ -343,66 +333,83 @@ function EasySwitchManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Список communities с карточками шлюзов */}
|
{/* Список серверов с communities и шлюзами */}
|
||||||
{filteredCommunities.length === 0 ? (
|
{filteredServers.length === 0 ? (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-body text-center py-5">
|
<div className="card-body text-center py-5">
|
||||||
<IconFilter size={48} className="text-muted mb-3" />
|
<IconFilter size={48} className="text-muted mb-3" />
|
||||||
<h3 className="text-muted">Ничего не найдено</h3>
|
<h3 className="text-muted">Ничего не найдено</h3>
|
||||||
<p className="text-muted">
|
<p className="text-muted">
|
||||||
{searchQuery ? 'Попробуйте изменить критерии поиска' : 'Нет доступных communities'}
|
{searchQuery ? 'Попробуйте изменить критерии поиска' : 'Нет доступных серверов'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{filteredCommunities.map((comm) => {
|
{filteredServers.map((server) => {
|
||||||
const parsed = parseCommunity(comm.community);
|
const serverMeta = getServerMetadata(server.name);
|
||||||
const activeGw = activeGateways[comm.community];
|
const country = serverMeta?.country || '';
|
||||||
|
const flag = countryToFlag(country);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={comm.community} className="card mb-3">
|
<div key={server.id} className="card mb-3">
|
||||||
<div className="card-header py-2">
|
{/* Заголовок сервера */}
|
||||||
<div className="d-flex align-items-center justify-content-between">
|
<div className="card-header py-2" style={{ backgroundColor: 'rgba(32, 107, 196, 0.05)' }}>
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<span className="badge bg-blue-lt text-blue me-2" style={{ fontSize: '0.85rem' }}>
|
{flag && <span className="me-2" style={{ fontSize: '1.5rem' }}>{flag}</span>}
|
||||||
|
<IconServer size={20} className="me-2 text-primary" />
|
||||||
|
<h3 className="card-title mb-0 me-2">{server.name}</h3>
|
||||||
|
{server.description && (
|
||||||
|
<span className="text-muted small">— {server.description}</span>
|
||||||
|
)}
|
||||||
|
<span className="badge bg-blue-lt text-blue ms-auto">
|
||||||
|
{server.communities.length} {server.communities.length === 1 ? 'community' : 'communities'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card-body p-3">
|
||||||
|
{/* Communities для этого сервера */}
|
||||||
|
{server.communities.map((comm) => {
|
||||||
|
const activeKey = `${server.id}:${comm.community}`;
|
||||||
|
const activeGw = activeGateways[activeKey];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={comm.community} className="mb-3">
|
||||||
|
{/* Заголовок community */}
|
||||||
|
<div className="mb-2">
|
||||||
|
<span className="badge bg-blue-lt text-blue me-2" style={{ fontSize: '0.8rem' }}>
|
||||||
{comm.community}
|
{comm.community}
|
||||||
</span>
|
</span>
|
||||||
<span className="fw-bold me-2">{comm.description}</span>
|
<span className="fw-medium">{comm.description}</span>
|
||||||
<span className="text-muted small">
|
<span className="text-muted small ms-2">
|
||||||
({comm.gateways.length})
|
({comm.gateways.length})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="card-body py-2">
|
|
||||||
{/* Сетка карточек шлюзов */}
|
{/* Сетка карточек шлюзов */}
|
||||||
<div className="row g-2">
|
<div className="row g-2">
|
||||||
{comm.gateways.map((gw, idx) => {
|
{comm.gateways.map((gw, idx) => {
|
||||||
const isActive = activeGw === gw.gateway;
|
const isActive = activeGw === gw.name;
|
||||||
const serverMeta = getServerMetadata(gw.serverName);
|
const isFastest = idx === 0;
|
||||||
const country = serverMeta?.country || '';
|
const ping = 20 + idx * 10 + Math.floor(Math.random() * 15);
|
||||||
const flag = countryToFlag(country);
|
|
||||||
const isFastest = idx === 0; // Первый считаем самым быстрым
|
|
||||||
const ping = 20 + idx * 10 + Math.floor(Math.random() * 15); // Более реалистичный пинг
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={`${gw.gateway}-${gw.serverId}`} className="col-sm-6 col-md-4 col-lg-3">
|
<div key={gw.name} className="col-6 col-sm-4 col-md-3 col-lg-2">
|
||||||
<div
|
<div
|
||||||
className={`card cursor-pointer h-100 ${isActive ? 'bg-primary text-white' : ''}`}
|
className={`card cursor-pointer h-100 ${isActive ? 'bg-primary text-white' : ''}`}
|
||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.15s ease',
|
transition: 'all 0.15s ease',
|
||||||
border: isActive ? '2px solid var(--tblr-primary)' : '1px solid rgba(0,0,0,0.1)',
|
border: isActive ? '2px solid var(--tblr-primary)' : '1px solid rgba(0,0,0,0.1)',
|
||||||
borderRadius: '8px',
|
borderRadius: '6px',
|
||||||
boxShadow: isActive ? '0 4px 12px rgba(32, 107, 196, 0.3)' : 'none'
|
boxShadow: isActive ? '0 3px 8px rgba(32, 107, 196, 0.3)' : 'none'
|
||||||
}}
|
}}
|
||||||
onClick={() => handleGatewaySelect(comm.community, gw.gateway)}
|
onClick={() => handleGatewaySelect(server.id, comm.community, gw.name)}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
|
e.currentTarget.style.boxShadow = '0 2px 6px rgba(0,0,0,0.1)';
|
||||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
@@ -412,46 +419,38 @@ function EasySwitchManager() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="card-body p-3">
|
<div className="card-body p-2">
|
||||||
{/* Флаг и галочка */}
|
{/* Галочка и метка Fastest */}
|
||||||
<div className="d-flex align-items-start justify-content-between mb-2">
|
<div className="d-flex align-items-start justify-content-between mb-1">
|
||||||
{flag ? (
|
{isFastest && !isActive && (
|
||||||
<span style={{ fontSize: '2rem', lineHeight: 1 }}>{flag}</span>
|
<span className="badge bg-yellow-lt text-yellow d-inline-flex align-items-center" style={{ fontSize: '0.65rem', padding: '2px 4px' }}>
|
||||||
) : (
|
<IconBolt size={10} className="me-1" />
|
||||||
<IconServer size={32} className={isActive ? 'text-white' : 'text-muted'} />
|
Fast
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{!isFastest && <span></span>}
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<div
|
<div
|
||||||
className="bg-white text-primary rounded-circle d-flex align-items-center justify-content-center"
|
className="bg-white text-primary rounded-circle d-flex align-items-center justify-content-center"
|
||||||
style={{ width: '24px', height: '24px' }}
|
style={{ width: '18px', height: '18px' }}
|
||||||
>
|
>
|
||||||
<IconCheck size={16} />
|
<IconCheck size={12} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Метка Fastest */}
|
|
||||||
{isFastest && !isActive && (
|
|
||||||
<div className="mb-2">
|
|
||||||
<span className="badge bg-yellow-lt text-yellow d-inline-flex align-items-center">
|
|
||||||
<IconBolt size={14} className="me-1" />
|
|
||||||
Fastest
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Название шлюза */}
|
{/* Название шлюза */}
|
||||||
<div className={`fw-bold mb-1 ${isActive ? 'text-white' : ''}`} style={{ fontSize: '0.95rem' }}>
|
<div className={`fw-bold mb-1 ${isActive ? 'text-white' : ''}`} style={{ fontSize: '0.8rem' }}>
|
||||||
{flag && country ? `${country}` : gw.gateway}
|
{gw.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Протокол */}
|
{/* Протокол */}
|
||||||
<div className={`small mb-2 ${isActive ? 'text-white' : 'text-muted'}`} style={{ opacity: isActive ? 0.85 : 1 }}>
|
<div className={`small ${isActive ? 'text-white' : 'text-muted'}`} style={{ fontSize: '0.7rem', opacity: isActive ? 0.85 : 1 }}>
|
||||||
{serverMeta?.provider || 'vless / xudp'}
|
{serverMeta?.provider || 'vless'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Метрика (пинг) */}
|
{/* Метрика (пинг) */}
|
||||||
<div className={`fw-bold ${isActive ? 'text-white' : 'text-success'}`} style={{ fontSize: '1.25rem' }}>
|
<div className={`fw-bold mt-1 ${isActive ? 'text-white' : 'text-success'}`} style={{ fontSize: '1rem' }}>
|
||||||
{ping}
|
{ping}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -461,6 +460,9 @@ function EasySwitchManager() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user