148 lines
5.4 KiB
Vue
148 lines
5.4 KiB
Vue
<template>
|
||
<div v-if="ipData" class="bg-gray-50/80 dark:bg-gray-800/30 border-b border-gray-200 dark:border-gray-700 text-sm backdrop-blur-sm">
|
||
<div class="container mx-auto px-6 py-2 md:px-12">
|
||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||
|
||
<!-- Блок с IP и защитой -->
|
||
<div class="flex items-center gap-3">
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-gray-500 dark:text-gray-400 text-xs">🌐 Ваш IP:</span>
|
||
<code class="font-mono text-xs md:text-sm font-medium bg-white dark:bg-gray-900 px-2 py-0.5 rounded border border-gray-200 dark:border-gray-700">
|
||
{{ ipData.ip }}
|
||
</code>
|
||
</div>
|
||
<span class="inline-flex items-center gap-2 text-xs px-2 py-0.5 rounded-full font-medium" :class="isProtected ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' : 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400'">
|
||
<span class="relative flex h-1.5 w-1.5">
|
||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75" :class="isProtected ? 'bg-green-500' : 'bg-amber-500'"></span>
|
||
<span class="relative inline-flex rounded-full h-1.5 w-1.5" :class="isProtected ? 'bg-green-600' : 'bg-amber-600'"></span>
|
||
</span>
|
||
{{ isProtected ? 'Защищено' : 'Не защищено' }}
|
||
</span>
|
||
</div>
|
||
|
||
<!-- Локация с флагом -->
|
||
<div class="flex items-center justify-center gap-1 text-xs text-gray-500 dark:text-gray-400">
|
||
<!-- Локальный SVG-флаг -->
|
||
<img
|
||
v-if="countryCode"
|
||
:src="`https://cdn.jsdelivr.net/npm/flag-icons@7.2.3/flags/4x3/${countryCode}.svg`"
|
||
:alt="countryCode"
|
||
class="h-3 w-auto"
|
||
/>
|
||
|
||
<template v-if="ipData.location?.city">
|
||
<span class="hidden sm:inline">{{ ipData.location.city }}</span>
|
||
<span class="text-gray-400 hidden sm:inline">•</span>
|
||
</template>
|
||
|
||
<span v-if="ipData.location?.country" class="text-gray-600 dark:text-gray-300 font-medium hidden sm:inline">
|
||
{{ ipData.location.country }}
|
||
</span>
|
||
|
||
<template v-if="ipData.company?.name">
|
||
<span class="text-gray-400 hidden md:inline">•</span>
|
||
<span class="hidden md:inline text-gray-400">
|
||
{{ truncateText(ipData.company.name, 30) }}
|
||
</span>
|
||
</template>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Лоадер -->
|
||
<div v-else-if="loading" class="bg-gray-50/80 dark:bg-gray-800/30 border-b border-gray-200 dark:border-gray-700">
|
||
<div class="container mx-auto px-6 py-2 md:px-12">
|
||
<div class="flex items-center gap-3">
|
||
<div class="h-5 w-28 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||
<div class="h-5 w-16 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Ошибка -->
|
||
<div v-else-if="error" class="bg-amber-50 dark:bg-amber-900/10 border-b border-amber-200 dark:border-amber-800/50">
|
||
<div class="container mx-auto px-6 py-1.5 md:px-12">
|
||
<div class="flex items-center gap-2 text-xs text-amber-700 dark:text-amber-400">
|
||
<span>⚠️</span>
|
||
<span>Не удалось определить IP</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { getAllCountries } from '@tw-labs/countries'
|
||
|
||
const ipData = ref(null)
|
||
const loading = ref(true)
|
||
const error = ref(false)
|
||
|
||
// Список стран для маппинга названия -> код
|
||
const countriesList = getAllCountries()
|
||
|
||
function findCountryByName(countryName) {
|
||
if (!countryName) return null
|
||
const found = countriesList.find(c => c.label.toLowerCase() === countryName.toLowerCase())
|
||
if (found) return found
|
||
// частичное совпадение (например "Russian Federation" vs "Russia")
|
||
const partial = countriesList.find(c =>
|
||
countryName.toLowerCase().includes(c.label.toLowerCase()) ||
|
||
c.label.toLowerCase().includes(countryName.toLowerCase())
|
||
)
|
||
return partial || null
|
||
}
|
||
|
||
async function fetchIp() {
|
||
loading.value = true
|
||
error.value = false
|
||
|
||
try {
|
||
const response = await fetch('/api/v1/ip')
|
||
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
||
|
||
const data = await response.json()
|
||
const countryName = data.location?.country
|
||
const foundCountry = findCountryByName(countryName)
|
||
const countryCode = foundCountry?.code || null
|
||
|
||
ipData.value = {
|
||
...data,
|
||
location: {
|
||
...data.location,
|
||
computed_country_code: countryCode
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('IP fetch error:', err)
|
||
error.value = true
|
||
ipData.value = null
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const isProtected = computed(() => {
|
||
if (!ipData.value) return false
|
||
return ipData.value.security?.vpn ||
|
||
ipData.value.security?.proxy ||
|
||
ipData.value.security?.tor ||
|
||
ipData.value.security?.hosting
|
||
})
|
||
|
||
const countryCode = computed(() => {
|
||
const code = ipData.value?.location?.computed_country_code
|
||
return code ? code.toLowerCase() : null
|
||
})
|
||
|
||
function truncateText(text, maxLength) {
|
||
if (!text) return ''
|
||
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchIp()
|
||
})
|
||
</script> |