Enhance Mihomo proxy groups component with improved data handling and UI updates
Publish telemt-api gateway Docker image / test (push) Successful in 28s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 2m14s

- Introduced a stable order for selectable proxy groups to minimize DOM changes during JSON updates.
- Added a refresh icon to the update button for better user feedback during loading states.
- Enhanced error handling to prevent UI flickering when fetching proxy data.
- Updated layout in the Mihomo page to utilize a grid system for better responsiveness and organization.
This commit is contained in:
Denozordec
2026-03-31 17:18:23 +07:00
parent 398c776238
commit 55354d3270
2 changed files with 26 additions and 5 deletions
@@ -6,6 +6,7 @@
import DataQueryState from '$lib/components/fleet/data-query-state.svelte'; import DataQueryState from '$lib/components/fleet/data-query-state.svelte';
import { Skeleton } from '$lib/components/ui/skeleton/index.js'; import { Skeleton } from '$lib/components/ui/skeleton/index.js';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
let { let {
alias, alias,
@@ -39,8 +40,8 @@
try { try {
proxiesData = await fetchMihomoJson<MihomoProxiesResponse>(a, 'proxies'); proxiesData = await fetchMihomoJson<MihomoProxiesResponse>(a, 'proxies');
} catch (e) { } catch (e) {
proxiesData = null;
proxiesErr = e instanceof ApiError ? e.message : String(e); proxiesErr = e instanceof ApiError ? e.message : String(e);
// не затираем успешный снимок при сбое обновления — без мерцания UI
} finally { } finally {
proxiesLoading = false; proxiesLoading = false;
} }
@@ -59,6 +60,15 @@
return false; return false;
} }
/** Стабильный порядок групп — меньше скачков DOM при обновлении JSON. */
let selectableGroups = $derived.by((): [string, MihomoProxyEntry][] => {
const p = proxiesData?.proxies;
if (!p) return [];
return Object.entries(p)
.filter(([_, v]) => isSelectableGroup(v))
.sort(([a], [b]) => a.localeCompare(b));
});
function lastDelay(p: MihomoProxyEntry): number | null { function lastDelay(p: MihomoProxyEntry): number | null {
const h = p.history; const h = p.history;
if (!h?.length) return null; if (!h?.length) return null;
@@ -139,11 +149,20 @@
onclick={() => loadProxies()} onclick={() => loadProxies()}
disabled={proxiesLoading || !alias} disabled={proxiesLoading || !alias}
> >
<RefreshCwIcon
class={cn('mr-1 size-3.5 shrink-0', proxiesLoading && 'animate-spin')}
aria-hidden="true"
/>
Обновить Обновить
</Button> </Button>
</div> </div>
<DataQueryState err={proxiesErr} showSkeleton={proxiesLoading} isEmpty={false}> <DataQueryState
err={proxiesErr}
hasStaleData={proxiesData != null && proxiesErr != null}
showSkeleton={proxiesLoading && proxiesData === null}
isEmpty={false}
>
{#snippet skeleton()} {#snippet skeleton()}
{#if compact} {#if compact}
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1">
@@ -162,7 +181,7 @@
{#snippet children()} {#snippet children()}
{#if proxiesData?.proxies} {#if proxiesData?.proxies}
<div class={cn('flex flex-col', compact ? 'gap-3' : '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)} {#each selectableGroups as [gName, group] (gName)}
{#if compact} {#if compact}
<div class="bg-muted/20 rounded-lg border px-2 py-2 sm:px-3"> <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="mb-1.5 flex flex-wrap items-center justify-between gap-x-2 gap-y-1">
+4 -2
View File
@@ -329,12 +329,14 @@
Список групп подгружается автоматически. Клик по ячейке — выбор узла; «ping» — задержка. Список групп подгружается автоматически. Клик по ячейке — выбор узла; «ping» — задержка.
</p> </p>
</div> </div>
<div class="flex flex-col gap-3"> <div
class="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3"
>
{#each rows.filter((r) => r.metaStatus === 'ok') as r (r.alias)} {#each rows.filter((r) => r.metaStatus === 'ok') as r (r.alias)}
<Card.Root <Card.Root
id="mihomo-proxies-{encodeURIComponent(r.alias)}" id="mihomo-proxies-{encodeURIComponent(r.alias)}"
data-size="sm" data-size="sm"
class="border-l-4 border-l-primary/30 scroll-mt-20 shadow-sm" class="border-l-4 border-l-primary/30 min-h-0 min-w-0 scroll-mt-20 shadow-sm"
> >
<Card.Header class="space-y-0 pb-1 pt-3"> <Card.Header class="space-y-0 pb-1 pt-3">
<Card.Title class="text-sm font-semibold">{r.alias}</Card.Title> <Card.Title class="text-sm font-semibold">{r.alias}</Card.Title>