Enhance Mihomo proxy groups component with improved data handling and UI updates
- 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:
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user