@@ -1,18 +1,19 @@
< template >
< template >
< section class = "px-6 py-20 md:px-12 max-w-7xl mx-auto" >
< section class = "px-6 py-20 md:px-12 max-w-7xl mx-auto" >
< h2 class = "text-3xl md:text-4xl font-bold text-center mb-14" >
< div class = "mb-14" >
Статус серверов
< h2 class = "text-3xl md:text-4xl font-bold text-center" >
< / h2 >
Статус серверов
< / h2 >
< div class = "flex justify-end mb-4 " >
< div class = "flex justify-c enter mt-6 " >
< button
< button
@click ="refreshData"
@click ="refreshData"
:disabled = "loading"
:disabled = "loading"
class = "px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
class = "px-5 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-x l text-sm font-medium transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 cursor-pointer "
>
>
< RefreshCw :size = "16" : class = "{ 'animate-spin': loading }" / >
< RefreshCw :size = "16" : class = "{ 'animate-spin': loading }" / >
{ { loading ? 'Обновление...' : 'Обновить все ' } }
{ { loading ? 'Обновление...' : 'Обновить пинг ' } }
< / button >
< / button >
< / div >
< / div >
< / div >
< div v-if = "loading && !hosts.length" class="flex justify-center py-12" >
< div v-if = "loading && !hosts.length" class="flex justify-center py-12" >
@@ -35,18 +36,24 @@
class = "h-5 w-auto rounded-sm"
class = "h-5 w-auto rounded-sm"
/ >
/ >
< span class = "font-semibold text-gray-800 dark:text-white" > { { host . name } } < / span >
< span class = "font-semibold text-gray-800 dark:text-white" > { { host . name } } < / span >
< span
class = "text-[10px] px-2 py-0.5 rounded-full font-medium"
<!-- Индикатор состояния ( вместо online / offline ) -- >
: class = "host.isOnline ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
< span class = "relative flex h-3 w-3" >
>
< span
{ { host . isOnline ? 'online' : 'offline' } }
class = "animate-ping absolute inline-flex h-full w-full rounded-full opacity-75"
: class = "host.isOnline ? 'bg-green-500' : 'bg-red-500'"
> < / span >
< span
class = "relative inline-flex rounded-full h-3 w-3"
: class = "host.isOnline ? 'bg-green-600' : 'bg-red-600'"
> < / span >
< / span >
< / span >
< / div >
< / div >
< div class = "flex items-center gap-4 " >
< div class = "flex items-center gap-3 " >
< span
< span
v-if = "host.ping !== undefined && host.ping !== null"
v-if = "host.ping !== undefined && host.ping !== null"
class = "font-mono font-semibold flex items-center gap-1 min-w-[60px] justify-end "
class = "font-mono font-semibold flex items-center gap-1.5 "
: class = "{
: class = "{
'text-green-600': host.ping < 200,
'text-green-600': host.ping < 200,
'text-yellow-600': host.ping >= 200 && host.ping < 500,
'text-yellow-600': host.ping >= 200 && host.ping < 500,
@@ -56,20 +63,7 @@
< Wifi :size = "14" / >
< Wifi :size = "14" / >
{ { host . ping } } ms
{ { host . ping } } ms
< / span >
< / span >
< span v-else class = "text-gray-300 min-w-[60px] text-right " > — < / span >
< span v-else class = "text-gray-300 font-mono " > — < / span >
< button
@click ="pingSingleHost(host)"
:disabled = "host.pinging"
class = "text-xs px-3 py-1 rounded-lg transition font-medium"
: class = "[
host.isOnline && !host.pinging
? 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
: 'bg-gray-100 dark:bg-gray-800 text-gray-400 cursor-not-allowed'
]"
>
{ { host . pinging ? '⏳' : 'Пинг' } }
< / button >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
@@ -77,40 +71,60 @@
< / template >
< / template >
< script setup >
< script setup >
import { ref , computed , onMounted , onUnmounted } from 'vue'
import { ref , computed , onMounted } from 'vue'
import { Wifi , RefreshCw } from '@lucide/vue'
import { Wifi , RefreshCw } from '@lucide/vue'
import { getAllCountries } from '@tw-labs/countries'
import { getAllCountries } from '@tw-labs/countries'
const hosts = ref ( [ ] )
const hosts = ref ( [ ] )
const loading = ref ( true )
const loading = ref ( true )
const error = ref ( null )
const error = ref ( null )
let interval = null
const countriesList = getAllCountries ( )
const countriesList = getAllCountries ( )
// Поиск страны по первому слову в названии
// Ручной маппинг для проблемных стран
const countryOverrides = {
'United States' : 'us' ,
'USA' : 'us' ,
'U.S.A.' : 'us' ,
'America' : 'us' ,
'United Arab Emirates' : 'ae' ,
'UAE' : 'ae' ,
}
const findCountryByFirstWord = ( name ) => {
const findCountryByFirstWord = ( name ) => {
if ( ! name ) return null
if ( ! name ) return null
// Берём первое слово из названия
// 1. Проверяем ручные override
const override = countryOverrides [ name ]
if ( override ) {
return { code : override . toUpperCase ( ) }
}
// 2. Ищем по первому слову
const firstWord = name . split ( ' ' ) [ 0 ]
const firstWord = name . split ( ' ' ) [ 0 ]
if ( ! firstWord ) return null
if ( ! firstWord ) return null
// Ищем т очное совпадение
// Т очное совпадение
const exact = countriesList . find ( c =>
const exact = countriesList . find ( c =>
c . label . toLowerCase ( ) === firstWord . toLowerCase ( )
c . label . toLowerCase ( ) === firstWord . toLowerCase ( )
)
)
if ( exact ) return exact
if ( exact ) return exact
// Ищем ч астичное совпадение
// Ч астичное совпадение
const partial = countriesList . find ( c =>
const partial = countriesList . find ( c =>
firstWord . toLowerCase ( ) . includes ( c . label . toLowerCase ( ) ) ||
firstWord . toLowerCase ( ) . includes ( c . label . toLowerCase ( ) ) ||
c . label . toLowerCase ( ) . includes ( firstWord . toLowerCase ( ) )
c . label . toLowerCase ( ) . includes ( firstWord . toLowerCase ( ) )
)
)
return partial || null
if ( partial ) return partial
// 3. Пробуем найти по полному имени
const fullMatch = countriesList . find ( c =>
name . toLowerCase ( ) . includes ( c . label . toLowerCase ( ) ) ||
c . label . toLowerCase ( ) . includes ( name . toLowerCase ( ) )
)
return fullMatch || null
}
}
// Сортировка по пингу
const sortedHosts = computed ( ( ) => {
const sortedHosts = computed ( ( ) => {
return [ ... hosts . value ] . sort ( ( a , b ) => {
return [ ... hosts . value ] . sort ( ( a , b ) => {
if ( a . ping === null && b . ping === null ) return 0
if ( a . ping === null && b . ping === null ) return 0
@@ -120,18 +134,6 @@ const sortedHosts = computed(() => {
} )
} )
} )
} )
// Пинг одного хоста
const pingSingleHost = async ( host ) => {
if ( host . pinging || ! host . isOnline ) return
host . pinging = true
host . ping = null
const result = await pingHost ( host )
host . ping = result
host . pinging = false
}
// Пинг через fetch
const pingHost = async ( host ) => {
const pingHost = async ( host ) => {
const url = ` https:// ${ host . address } : ${ host . port } `
const url = ` https:// ${ host . address } : ${ host . port } `
const start = performance . now ( )
const start = performance . now ( )
@@ -149,19 +151,6 @@ const pingHost = async (host) => {
}
}
}
}
// Пинг всех хостов
const pingAllHosts = async ( ) => {
const pings = await Promise . all (
hosts . value . map ( async ( h ) => {
if ( ! h . isOnline ) return null
return await pingHost ( h )
} )
)
hosts . value . forEach ( ( h , i ) => {
if ( h . isOnline ) h . ping = pings [ i ]
} )
}
const fetchHosts = async ( ) => {
const fetchHosts = async ( ) => {
try {
try {
const res = await fetch ( '/api/v1/hosts' )
const res = await fetch ( '/api/v1/hosts' )
@@ -175,11 +164,8 @@ const fetchHosts = async () => {
countryCode : country ? . code ? . toLowerCase ( ) || null ,
countryCode : country ? . code ? . toLowerCase ( ) || null ,
isOnline : true ,
isOnline : true ,
ping : null ,
ping : null ,
pinging : false ,
}
}
} )
} )
await pingAllHosts ( )
} catch {
} catch {
error . value = 'Ошибка загрузки'
error . value = 'Ошибка загрузки'
} finally {
} finally {
@@ -189,13 +175,39 @@ const fetchHosts = async () => {
const refreshData = async ( ) => {
const refreshData = async ( ) => {
loading . value = true
loading . value = true
await fetchHosts ( )
error . value = null
loading . value = false
try {
const res = await fetch ( '/api/v1/hosts' )
if ( ! res . ok ) throw new Error ( )
const data = await res . json ( )
hosts . value = data . map ( ( h ) => {
const country = findCountryByFirstWord ( h . name )
return {
... h ,
countryCode : country ? . code ? . toLowerCase ( ) || null ,
isOnline : true ,
ping : null ,
}
} )
const pings = await Promise . all ( hosts . value . map ( h => pingHost ( h ) ) )
hosts . value . forEach ( ( h , i ) => {
if ( h . isOnline ) h . ping = pings [ i ]
} )
} catch {
error . value = 'Ошибка обновления'
} finally {
loading . value = false
}
}
}
onMounted ( ( ) => {
// При первом открытии — загружаем и пингуем
fetchHosts ( )
onMounted ( async ( ) => {
interval = setInterval ( refreshData , 30000 )
await fetchHosts ( )
// Автоматический пинг только при первом открытии
await refreshData ( )
} )
} )
onUnmounted ( ( ) => clearInterval ( interval ) )
< / script >
< / script >