Refactor Mihomo proxy groups component for improved layout and functionality
- Replaced the lazy loading mechanism with a compact mode for better user experience and performance. - Enhanced the layout of proxy groups to support a more flexible display, accommodating both compact and standard views. - Updated button styles and descriptions for clarity, ensuring a more intuitive interaction with proxy nodes. - Adjusted the overall structure of the Mihomo page to streamline the presentation of proxy data and improve accessibility.
This commit is contained in:
@@ -5,17 +5,17 @@
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
import DataQueryState from '$lib/components/fleet/data-query-state.svelte';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
alias,
|
||||
lazy = false
|
||||
/** Плотная сетка мелких ячеек — для флота и списков из многих нод. */
|
||||
compact = false
|
||||
}: {
|
||||
alias: string;
|
||||
/** Не запрашивать /proxies до явного нажатия «Загрузить» / «Обновить». */
|
||||
lazy?: boolean;
|
||||
compact?: boolean;
|
||||
} = $props();
|
||||
|
||||
/** Mihomo hub/route/proxies.go: без query `url` URLTest часто даёт delay=0 → HTTP 503. */
|
||||
const MIHOMO_DELAY_DEFAULT_URL = 'https://www.gstatic.com/generate_204';
|
||||
|
||||
let proxiesLoading = $state(false);
|
||||
@@ -48,11 +48,10 @@
|
||||
|
||||
$effect(() => {
|
||||
const a = alias;
|
||||
if (!a || lazy) return;
|
||||
if (!a) return;
|
||||
void loadProxies();
|
||||
});
|
||||
|
||||
/** Группы, у которых есть `all` + `now` и PUT /proxies/{name} переключает узел (как в Clash/Mihomo). */
|
||||
function isSelectableGroup(p: MihomoProxyEntry) {
|
||||
const t = (p.type ?? '').toLowerCase();
|
||||
if (t === 'selector' || t === 'urltest' || t === 'fallback') return true;
|
||||
@@ -127,91 +126,178 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mb-4 flex flex-wrap items-center justify-end gap-2">
|
||||
{#if lazy && !proxiesData && !proxiesLoading}
|
||||
<Button variant="default" size="sm" onclick={() => loadProxies()}>Загрузить прокси</Button>
|
||||
{/if}
|
||||
<Button variant="outline" size="sm" onclick={() => loadProxies()} disabled={proxiesLoading || !alias}>
|
||||
<div
|
||||
class={cn(
|
||||
'flex flex-wrap items-center justify-end gap-1.5',
|
||||
compact ? 'mb-2' : 'mb-4'
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class={cn(compact && 'h-7 px-2 text-xs')}
|
||||
onclick={() => loadProxies()}
|
||||
disabled={proxiesLoading || !alias}
|
||||
>
|
||||
Обновить
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DataQueryState err={proxiesErr} showSkeleton={proxiesLoading} isEmpty={false}>
|
||||
{#snippet skeleton()}
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each Array.from({ length: 6 }) as _, i (i)}
|
||||
<Skeleton class="h-32 rounded-xl" />
|
||||
{/each}
|
||||
</div>
|
||||
{#if compact}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each Array.from({ length: 12 }) as _, i (i)}
|
||||
<Skeleton class="h-7 w-24 rounded-md" />
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each Array.from({ length: 6 }) as _, i (i)}
|
||||
<Skeleton class="h-32 rounded-xl" />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#snippet children()}
|
||||
{#if proxiesData?.proxies}
|
||||
<div class="flex flex-col gap-8">
|
||||
<div class={cn('flex flex-col', compact ? 'gap-3' : 'gap-8')}>
|
||||
{#each Object.entries(proxiesData.proxies).filter(([_, v]) => isSelectableGroup(v)) as [gName, group] (gName)}
|
||||
<div class="space-y-3">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">{gName}</h2>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Сейчас: <span class="text-foreground">{group.now ?? '—'}</span>
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onclick={() => pingGroup(gName)}
|
||||
disabled={testingGroup === gName}
|
||||
>
|
||||
{testingGroup === gName ? 'Проверка…' : 'Проверить все'}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each group.all ?? [] as nodeName (nodeName)}
|
||||
{@const node = proxiesData.proxies?.[nodeName]}
|
||||
{@const d = node ? lastDelay(node) : null}
|
||||
{@const active = group.now === nodeName}
|
||||
<div
|
||||
class="bg-card text-card-foreground rounded-xl border p-4 transition-shadow {active
|
||||
? 'ring-primary ring-2'
|
||||
: ''}"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left"
|
||||
onclick={() => selectProxy(gName, nodeName)}
|
||||
disabled={switching === gName}
|
||||
{#if compact}
|
||||
<div class="bg-muted/20 rounded-lg border px-2 py-2 sm:px-3">
|
||||
<div class="mb-1.5 flex flex-wrap items-center justify-between gap-x-2 gap-y-1">
|
||||
<div class="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0">
|
||||
<span class="text-xs font-semibold tracking-wide uppercase text-muted-foreground"
|
||||
>{gName}</span
|
||||
>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<span class="font-medium">{nodeName}</span>
|
||||
<Badge variant="outline">{node?.type ?? '—'}</Badge>
|
||||
</div>
|
||||
<div
|
||||
class="mt-2 text-sm {d != null && d < 500 ? 'text-emerald-400' : 'text-muted-foreground'}"
|
||||
>
|
||||
{d != null ? `${d} ms` : '—'}
|
||||
</div>
|
||||
</button>
|
||||
<div class="mt-2 flex justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 px-2"
|
||||
onclick={() => pingOne(nodeName)}
|
||||
disabled={testing === nodeName}
|
||||
>
|
||||
Ping
|
||||
</Button>
|
||||
</div>
|
||||
<span class="text-muted-foreground text-[11px] sm:text-xs">
|
||||
→ <span class="text-foreground">{group.now ?? '—'}</span>
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
class="h-6 shrink-0 px-2 text-[11px]"
|
||||
onclick={() => pingGroup(gName)}
|
||||
disabled={testingGroup === gName}
|
||||
>
|
||||
{testingGroup === gName ? '…' : 'Все delay'}
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-wrap gap-1"
|
||||
role="group"
|
||||
aria-label="Узлы группы {gName}"
|
||||
>
|
||||
{#each group.all ?? [] as nodeName (nodeName)}
|
||||
{@const node = proxiesData.proxies?.[nodeName]}
|
||||
{@const d = node ? lastDelay(node) : null}
|
||||
{@const active = group.now === nodeName}
|
||||
<div
|
||||
class={cn(
|
||||
'bg-background/80 flex min-w-0 max-w-full items-stretch rounded border text-left text-xs shadow-sm',
|
||||
active ? 'border-primary ring-primary/30 ring-1' : 'border-border'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="hover:bg-muted/60 min-w-0 flex-1 px-2 py-1 text-left transition-colors disabled:opacity-50"
|
||||
onclick={() => selectProxy(gName, nodeName)}
|
||||
disabled={switching === gName}
|
||||
title="Выбрать {nodeName}"
|
||||
>
|
||||
<div class="truncate font-medium leading-tight">{nodeName}</div>
|
||||
<div
|
||||
class="text-muted-foreground mt-0.5 flex items-center justify-between gap-1 text-[10px] leading-none"
|
||||
>
|
||||
<span class="truncate">{node?.type ?? '—'}</span>
|
||||
<span
|
||||
class={cn(
|
||||
d != null && d < 500 ? 'text-emerald-500 shrink-0' : 'shrink-0'
|
||||
)}
|
||||
>
|
||||
{d != null ? `${d}` : '—'}<span class="text-muted-foreground/80">ms</span>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="text-muted-foreground hover:bg-muted hover:text-foreground border-border/80 border-l px-1.5 py-1 text-[10px] font-medium leading-none tabular-nums"
|
||||
onclick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
void pingOne(nodeName);
|
||||
}}
|
||||
disabled={testing === nodeName}
|
||||
title="Проверить задержку"
|
||||
>
|
||||
{testing === nodeName ? '…' : 'ping'}
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">{gName}</h2>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Сейчас: <span class="text-foreground">{group.now ?? '—'}</span>
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onclick={() => pingGroup(gName)}
|
||||
disabled={testingGroup === gName}
|
||||
>
|
||||
{testingGroup === gName ? 'Проверка…' : 'Проверить все'}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each group.all ?? [] as nodeName (nodeName)}
|
||||
{@const node = proxiesData.proxies?.[nodeName]}
|
||||
{@const d = node ? lastDelay(node) : null}
|
||||
{@const active = group.now === nodeName}
|
||||
<div
|
||||
class="bg-card text-card-foreground rounded-xl border p-4 transition-shadow {active
|
||||
? 'ring-primary ring-2'
|
||||
: ''}"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left"
|
||||
onclick={() => selectProxy(gName, nodeName)}
|
||||
disabled={switching === gName}
|
||||
>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<span class="font-medium">{nodeName}</span>
|
||||
<Badge variant="outline">{node?.type ?? '—'}</Badge>
|
||||
</div>
|
||||
<div
|
||||
class="mt-2 text-sm {d != null && d < 500 ? 'text-emerald-400' : 'text-muted-foreground'}"
|
||||
>
|
||||
{d != null ? `${d} ms` : '—'}
|
||||
</div>
|
||||
</button>
|
||||
<div class="mt-2 flex justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 px-2"
|
||||
onclick={() => pingOne(nodeName)}
|
||||
disabled={testing === nodeName}
|
||||
>
|
||||
Ping
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{:else if !lazy || proxiesLoading}
|
||||
<!-- lazy без данных: ждём клика; скелетон выше -->
|
||||
{:else}
|
||||
<p class="text-muted-foreground text-sm">Нажмите «Загрузить прокси», чтобы получить список групп.</p>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</DataQueryState>
|
||||
|
||||
@@ -245,8 +245,8 @@
|
||||
<Card.Header class="pb-2">
|
||||
<Card.Title class="text-base">Ноды</Card.Title>
|
||||
<Card.Description>
|
||||
Контроллер и метрики. Ниже на этой же странице — переключение прокси по каждой ноде; «Детально»
|
||||
ведёт на полный обзор с графиками.
|
||||
Контроллер и метрики. Ниже — компактные группы прокси (автозагрузка). «Детально» — обзор с
|
||||
графиками.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="overflow-x-auto px-2 sm:px-6">
|
||||
@@ -322,30 +322,30 @@
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="mt-10 space-y-4">
|
||||
<div class="mt-8 space-y-3">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold tracking-tight">Прокси по нодам</h2>
|
||||
<p class="text-muted-foreground mt-1 text-sm">
|
||||
Выбор узла в группах Mihomo на месте. Для каждой ноды список загружается по кнопке «Загрузить
|
||||
прокси», чтобы не запрашивать все конфиги сразу.
|
||||
<h2 class="text-base font-semibold tracking-tight">Прокси по нодам</h2>
|
||||
<p class="text-muted-foreground mt-0.5 text-xs">
|
||||
Список групп подгружается автоматически. Клик по ячейке — выбор узла; «ping» — задержка.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-8">
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each rows.filter((r) => r.metaStatus === 'ok') as r (r.alias)}
|
||||
<Card.Root
|
||||
id="mihomo-proxies-{encodeURIComponent(r.alias)}"
|
||||
data-size="sm"
|
||||
class="border-l-4 border-l-primary/30 scroll-mt-20 shadow-sm"
|
||||
>
|
||||
<Card.Header class="pb-2">
|
||||
<Card.Title class="text-lg">{r.alias}</Card.Title>
|
||||
<Card.Header class="space-y-0 pb-1 pt-3">
|
||||
<Card.Title class="text-sm font-semibold">{r.alias}</Card.Title>
|
||||
{#if r.meta?.controller_base}
|
||||
<Card.Description class="font-mono text-xs">
|
||||
<Card.Description class="font-mono text-[11px] leading-snug">
|
||||
{r.meta.controller_base}
|
||||
</Card.Description>
|
||||
{/if}
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<MihomoProxyGroups alias={r.alias} lazy={true} />
|
||||
<Card.Content class="px-3 pb-3 pt-0 sm:px-4">
|
||||
<MihomoProxyGroups alias={r.alias} compact={true} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{/each}
|
||||
|
||||
Reference in New Issue
Block a user