From e2d89f2d11a9771af34bee2a7ade8c7f41d4b48e Mon Sep 17 00:00:00 2001 From: Denozordec Date: Tue, 31 Mar 2026 16:51:26 +0700 Subject: [PATCH] Add Mihomo menu item and refactor page component - Introduced a new menu item for Mihomo in the app sidebar for easier navigation. - Refactored the Mihomo page component by removing unused imports and state variables, streamlining the code for better performance and readability. - Enhanced the overall structure of the Mihomo page to improve maintainability and user experience. --- web/src/lib/components/app-sidebar.svelte | 10 + .../mihomo/mihomo-proxy-groups.svelte | 217 +++++++++++ web/src/routes/mihomo/+page.svelte | 343 ++++++++++++++++++ .../servers/[alias]/mihomo/+page.svelte | 211 +---------- 4 files changed, 574 insertions(+), 207 deletions(-) create mode 100644 web/src/lib/components/mihomo/mihomo-proxy-groups.svelte create mode 100644 web/src/routes/mihomo/+page.svelte diff --git a/web/src/lib/components/app-sidebar.svelte b/web/src/lib/components/app-sidebar.svelte index 8029567..39a5c0d 100644 --- a/web/src/lib/components/app-sidebar.svelte +++ b/web/src/lib/components/app-sidebar.svelte @@ -104,6 +104,16 @@ {/snippet} + + + {#snippet child({ props })} + + + Mihomo + + {/snippet} + + {#if serverAliases.length > 0} diff --git a/web/src/lib/components/mihomo/mihomo-proxy-groups.svelte b/web/src/lib/components/mihomo/mihomo-proxy-groups.svelte new file mode 100644 index 0000000..44c7db7 --- /dev/null +++ b/web/src/lib/components/mihomo/mihomo-proxy-groups.svelte @@ -0,0 +1,217 @@ + + +
+ {#if lazy && !proxiesData && !proxiesLoading} + + {/if} + +
+ + + {#snippet skeleton()} +
+ {#each Array.from({ length: 6 }) as _, i (i)} + + {/each} +
+ {/snippet} + {#snippet children()} + {#if proxiesData?.proxies} +
+ {#each Object.entries(proxiesData.proxies).filter(([_, v]) => isSelectableGroup(v)) as [gName, group] (gName)} +
+
+
+

{gName}

+

+ Сейчас: {group.now ?? '—'} +

+
+ +
+
+ {#each group.all ?? [] as nodeName (nodeName)} + {@const node = proxiesData.proxies?.[nodeName]} + {@const d = node ? lastDelay(node) : null} + {@const active = group.now === nodeName} +
+ +
+ +
+
+ {/each} +
+
+ {/each} +
+ {:else if !lazy || proxiesLoading} + + {:else} +

Нажмите «Загрузить прокси», чтобы получить список групп.

+ {/if} + {/snippet} +
diff --git a/web/src/routes/mihomo/+page.svelte b/web/src/routes/mihomo/+page.svelte new file mode 100644 index 0000000..7b07281 --- /dev/null +++ b/web/src/routes/mihomo/+page.svelte @@ -0,0 +1,343 @@ + + +
+
+

Mihomo (флот)

+

+ Сводка по нодам и управление прокси. Опрос соединений каждые {POLL_MS / 1000} с. +

+
+
+ + + Сводка + Прокси по нодам + + + +
+
+ + + {#snippet empty()} +

+ В снимке агрегата нет серверов. Проверьте шлюз и конфигурацию servers. +

+ {/snippet} + {#snippet skeleton()} +
+ {#each Array.from({ length: 5 }) as _, i (i)} + + {/each} +
+ + {/snippet} + {#snippet children()} + {#if tab === 'summary' && rows.length > 0} +
+ + {#snippet icon()} + + + {#snippet icon()} + + + {#snippet icon()} + + + {#snippet icon()} + + + {#snippet icon()} + +
+ + + + Ноды + Контроллер, статус опроса, ссылка на детальную панель. + + + + + + Alias + Контроллер + Статус + Conn + Память + Трафик + + + + + {#each rows as r (r.alias)} + {@const b = statusBadge(r)} + + {r.alias} + + {r.meta?.controller_base ?? '—'} + + + {b.label} + {#if r.metaStatus !== 'ok' && r.metaMessage} +

{r.metaMessage}

+ {/if} + {#if r.pollErr} +

{r.pollErr}

+ {/if} +
+ + {r.metaStatus === 'ok' ? (r.connections ?? '—') : '—'} + + + {r.memory != null && r.memory > 0 ? formatBytes(r.memory) : '—'} + + + {#if r.metaStatus === 'ok' && (r.uploadTotal != null || r.downloadTotal != null)} + {formatBytes(r.uploadTotal ?? 0)} ↑
+ {formatBytes(r.downloadTotal ?? 0)} ↓ + {:else} + — + {/if} +
+ + {#if r.metaStatus === 'ok'} + + Детально + + {/if} + +
+ {/each} +
+
+
+
+ {:else if tab === 'proxies'} +
+ {#each rows.filter((r) => r.metaStatus === 'ok') as r (r.alias)} + + + {r.alias} + {#if r.meta?.controller_base} + + {r.meta.controller_base} + + {/if} + + + + + + {/each} + {#if rows.filter((r) => r.metaStatus === 'ok').length === 0} +

+ Нет нод с настроенным Mihomo. Проверьте конфиг шлюза и вкладку «Сводка». +

+ {/if} +
+ {/if} + {/snippet} +
diff --git a/web/src/routes/servers/[alias]/mihomo/+page.svelte b/web/src/routes/servers/[alias]/mihomo/+page.svelte index cd5d572..84e830b 100644 --- a/web/src/routes/servers/[alias]/mihomo/+page.svelte +++ b/web/src/routes/servers/[alias]/mihomo/+page.svelte @@ -2,34 +2,21 @@ import { page } from '$app/state'; import { browser } from '$app/environment'; import { onDestroy } from 'svelte'; - import { - ApiError, - fetchMihomoJson, - fetchMihomoMeta, - mihomoPut, - mihomoUrl, - mihomoWsUrl - } from '$lib/api/client.js'; - import type { - MihomoConnectionsResponse, - MihomoMetaResponse, - MihomoProxiesResponse, - MihomoProxyEntry - } from '$lib/api/mihomo-types.js'; + import { ApiError, fetchMihomoJson, fetchMihomoMeta, mihomoUrl, mihomoWsUrl } from '$lib/api/client.js'; + import type { MihomoConnectionsResponse, MihomoMetaResponse } from '$lib/api/mihomo-types.js'; import { formatBytes, formatRatePerSec } from '$lib/format.js'; import * as Card from '$lib/components/ui/card/index.js'; import KpiStatCard from '$lib/components/kpi-stat-card.svelte'; - import { Button } from '$lib/components/ui/button/index.js'; import ArrowBigUpIcon from '@lucide/svelte/icons/arrow-big-up'; import ArrowBigDownIcon from '@lucide/svelte/icons/arrow-big-down'; import CloudUploadIcon from '@lucide/svelte/icons/cloud-upload'; import CloudDownloadIcon from '@lucide/svelte/icons/cloud-download'; import PlugIcon from '@lucide/svelte/icons/plug'; import MemoryStickIcon from '@lucide/svelte/icons/memory-stick'; - import { Badge } from '$lib/components/ui/badge/index.js'; import * as Tabs from '$lib/components/ui/tabs/index.js'; import DataQueryState from '$lib/components/fleet/data-query-state.svelte'; import MihomoOverviewCharts from '$lib/components/mihomo/mihomo-overview-charts.svelte'; + import MihomoProxyGroups from '$lib/components/mihomo/mihomo-proxy-groups.svelte'; import { Skeleton } from '$lib/components/ui/skeleton/index.js'; const alias = $derived(page.params.alias ?? ''); @@ -62,19 +49,9 @@ if (!Number.isFinite(bytes) || bytes <= 0) return 0; return bytes / 1024; } - /** Mihomo hub/route/proxies.go: без query `url` URLTest часто даёт delay=0 → HTTP 503. */ - const MIHOMO_DELAY_DEFAULT_URL = 'https://www.gstatic.com/generate_204'; - let lastTick = $state(null); let pollTimer: ReturnType | null = null; - let proxiesLoading = $state(false); - let proxiesErr = $state(null); - let proxiesData = $state(null); - let switching = $state(null); - let testing = $state(null); - let testingGroup = $state(null); - /** Mihomo отдаёт upTotal/downTotal в потоке /traffic — используем их; иначе накапливаем из up/down. */ function recordRates( now: number, @@ -116,12 +93,6 @@ } } - function delayQuery(testUrl?: string) { - const u = - testUrl && String(testUrl).trim() !== '' ? String(testUrl).trim() : MIHOMO_DELAY_DEFAULT_URL; - return `timeout=5000&url=${encodeURIComponent(u)}`; - } - function sleep(ms: number, signal: AbortSignal): Promise { return new Promise((resolve, reject) => { if (signal.aborted) { @@ -520,101 +491,6 @@ }; }); - async function loadProxies() { - const a = alias; - if (!a) return; - proxiesLoading = true; - proxiesErr = null; - try { - proxiesData = await fetchMihomoJson(a, 'proxies'); - } catch (e) { - proxiesData = null; - proxiesErr = e instanceof ApiError ? e.message : String(e); - } finally { - proxiesLoading = false; - } - } - - $effect(() => { - const a = alias; - if (!a || tab !== 'proxies') 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; - if (t === 'loadbalance' && Array.isArray(p.all) && p.all.length > 0) return true; - return false; - } - - function lastDelay(p: MihomoProxyEntry): number | null { - const h = p.history; - if (!h?.length) return null; - return h[h.length - 1]?.delay ?? null; - } - - async function selectProxy(groupName: string, nodeName: string) { - const a = alias; - if (!a) return; - switching = groupName; - try { - await mihomoPut(a, `proxies/${encodeURIComponent(groupName)}`, { name: nodeName }); - await loadProxies(); - } catch (e) { - proxiesErr = e instanceof ApiError ? e.message : String(e); - } finally { - switching = null; - } - } - - async function pingOne(name: string) { - const a = alias; - if (!a) return; - testing = name; - try { - const testUrl = proxiesData?.proxies?.[name]?.testUrl; - await fetchMihomoJson( - a, - `proxies/${encodeURIComponent(name)}/delay?${delayQuery(testUrl)}` - ); - await loadProxies(); - } catch (e) { - proxiesErr = e instanceof ApiError ? e.message : String(e); - } finally { - testing = null; - } - } - - async function pingGroup(groupName: string) { - const a = alias; - if (!a) return; - testingGroup = groupName; - const groupTestUrl = proxiesData?.proxies?.[groupName]?.testUrl; - try { - await fetchMihomoJson( - a, - `group/${encodeURIComponent(groupName)}/delay?${delayQuery(groupTestUrl)}` - ); - await loadProxies(); - } catch { - try { - const g = proxiesData?.proxies?.[groupName]; - const names = g?.all ?? []; - for (const n of names) { - const u = proxiesData?.proxies?.[n]?.testUrl; - await fetchMihomoJson(a, `proxies/${encodeURIComponent(n)}/delay?${delayQuery(u)}`); - } - await loadProxies(); - } catch (e) { - proxiesErr = e instanceof ApiError ? e.message : String(e); - } - } finally { - testingGroup = null; - } - } - onDestroy(() => { if (pollTimer) clearInterval(pollTimer); }); @@ -734,84 +610,5 @@ {#if tab === 'proxies'} -
- -
- - {#snippet skeleton()} -
- {#each Array.from({ length: 6 }) as _, i (i)} - - {/each} -
- {/snippet} - {#snippet children()} - {#if proxiesData?.proxies} -
- {#each Object.entries(proxiesData.proxies).filter(([_, v]) => isSelectableGroup(v)) as [gName, group] (gName)} -
-
-
-

{gName}

-

- Сейчас: {group.now ?? '—'} -

-
- -
-
- {#each group.all ?? [] as nodeName (nodeName)} - {@const node = proxiesData.proxies?.[nodeName]} - {@const d = node ? lastDelay(node) : null} - {@const active = group.now === nodeName} -
- -
- -
-
- {/each} -
-
- {/each} -
- {/if} - {/snippet} -
+ {/if}