feat: Add toggle for grouping communities by tags in EasySwitchManager, enhancing user control over community display and improving organization.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m3s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m3s
This commit is contained in:
@@ -46,6 +46,7 @@ function EasySwitchManager() {
|
|||||||
const [initialGateways, setInitialGateways] = useState({}); // Исходное состояние для отслеживания изменений
|
const [initialGateways, setInitialGateways] = useState({}); // Исходное состояние для отслеживания изменений
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
const [expandedServers, setExpandedServers] = useState(new Set()); // Развернутые серверы
|
const [expandedServers, setExpandedServers] = useState(new Set()); // Развернутые серверы
|
||||||
|
const [groupByTags, setGroupByTags] = useState(true); // Группировка по тегам
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initData = async () => {
|
const initData = async () => {
|
||||||
@@ -492,6 +493,19 @@ function EasySwitchManager() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Переключатель "Группировка по тегам" */}
|
||||||
|
<div className="col-auto">
|
||||||
|
<label className="form-check form-switch mb-0">
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
checked={groupByTags}
|
||||||
|
onChange={(e) => setGroupByTags(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span className="form-check-label">Группировать по тегам</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Поиск */}
|
{/* Поиск */}
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<div className="input-icon">
|
<div className="input-icon">
|
||||||
@@ -622,8 +636,10 @@ function EasySwitchManager() {
|
|||||||
{/* Контент (сворачиваемый) */}
|
{/* Контент (сворачиваемый) */}
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="card-body p-3">
|
<div className="card-body p-3">
|
||||||
{/* Группируем communities по тегам */}
|
{/* Условный рендеринг: группировка по тегам или обычный вид */}
|
||||||
{(() => {
|
{groupByTags ? (
|
||||||
|
/* Группируем communities по тегам */
|
||||||
|
(() => {
|
||||||
// Группируем по тегам
|
// Группируем по тегам
|
||||||
const groupedByTags = new Map();
|
const groupedByTags = new Map();
|
||||||
const noTagCommunities = [];
|
const noTagCommunities = [];
|
||||||
@@ -921,7 +937,127 @@ function EasySwitchManager() {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})()}
|
})()
|
||||||
|
) : (
|
||||||
|
/* Обычный вид без группировки */
|
||||||
|
<div className="row g-3">
|
||||||
|
{filteredCommunitiesForServer.map((comm) => {
|
||||||
|
const activeKey = `${server.id}:${comm.value}`;
|
||||||
|
const activeGw = activeGateways[activeKey];
|
||||||
|
const hasFilterForComm = server.filtersMap.has(comm.value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={comm.value} className="col-12 col-md-6 col-lg-4">
|
||||||
|
<div className="mb-2">
|
||||||
|
{/* Заголовок community */}
|
||||||
|
<div className="mb-2">
|
||||||
|
<span className="badge bg-blue-lt text-blue me-2" style={{ fontSize: '0.8rem' }}>
|
||||||
|
{comm.value}
|
||||||
|
</span>
|
||||||
|
<span className="fw-medium">{comm.name || comm.description || ''}</span>
|
||||||
|
{hasFilterForComm && (
|
||||||
|
<span className="badge bg-green-lt text-green ms-2" style={{ fontSize: '0.7rem' }}>
|
||||||
|
настроен
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-muted small ms-2">
|
||||||
|
({server.gateways.length})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Сетка карточек шлюзов для этого community */}
|
||||||
|
<div className="row g-2">
|
||||||
|
{server.gateways.map((gw, idx) => {
|
||||||
|
const isActive = activeGw === gw.name;
|
||||||
|
const isFastest = idx === 0;
|
||||||
|
const ping = 20 + idx * 10 + Math.floor(Math.random() * 15);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={gw.name} className="col-12 col-sm-4">
|
||||||
|
<div
|
||||||
|
className={`card cursor-pointer h-100 ${isActive ? 'bg-primary text-white' : ''}`}
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
border: isActive ? '2px solid var(--tblr-primary)' : '1px solid rgba(0,0,0,0.1)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
boxShadow: isActive ? '0 3px 8px rgba(32, 107, 196, 0.3)' : 'none',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
onClick={() => handleGatewaySelect(server.id, comm.value, gw.name)}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (!isActive) {
|
||||||
|
e.currentTarget.style.boxShadow = '0 2px 6px rgba(0,0,0,0.1)';
|
||||||
|
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (!isActive) {
|
||||||
|
e.currentTarget.style.boxShadow = 'none';
|
||||||
|
e.currentTarget.style.transform = 'translateY(0)';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="card-body p-2">
|
||||||
|
{/* Галочка (абсолютное позиционирование) */}
|
||||||
|
{isActive && (
|
||||||
|
<div
|
||||||
|
className="bg-white text-primary rounded-circle d-flex align-items-center justify-content-center"
|
||||||
|
style={{
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
position: 'absolute',
|
||||||
|
top: '8px',
|
||||||
|
right: '8px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconCheck size={12} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Метка Fastest */}
|
||||||
|
<div className="mb-1" style={{ minHeight: '18px' }}>
|
||||||
|
{isFastest && !isActive && (
|
||||||
|
<span className="badge bg-yellow-lt text-yellow d-inline-flex align-items-center" style={{ fontSize: '0.65rem', padding: '2px 6px' }}>
|
||||||
|
<IconBolt size={10} className="me-1" />
|
||||||
|
Fast
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Название шлюза */}
|
||||||
|
<div className={`fw-bold mb-1 ${isActive ? 'text-white' : ''}`} style={{ fontSize: '0.85rem', lineHeight: '1.2' }}>
|
||||||
|
{gw.name}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Описание шлюза */}
|
||||||
|
{gw.comment && (
|
||||||
|
<div className={`small mb-1 ${isActive ? 'text-white' : 'text-muted'}`} style={{ fontSize: '0.7rem', opacity: isActive ? 0.8 : 1, lineHeight: '1.2' }}>
|
||||||
|
{gw.comment}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Протокол */}
|
||||||
|
<div className={`small mb-1 ${isActive ? 'text-white' : 'text-muted'}`} style={{ fontSize: '0.7rem', opacity: isActive ? 0.85 : 1 }}>
|
||||||
|
{serverMeta?.provider || 'vless'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Метрика (пинг) */}
|
||||||
|
<div className={`fw-bold mt-1 ${isActive ? 'text-white' : 'text-success'}`} style={{ fontSize: '1.1rem' }}>
|
||||||
|
{ping}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user