426 lines
14 KiB
Vue
426 lines
14 KiB
Vue
<template>
|
|
<div class="space-y-6">
|
|
<!-- 工具栏 -->
|
|
<div class="card p-4">
|
|
<div class="flex flex-wrap gap-2">
|
|
<button
|
|
@click="queryIP"
|
|
:disabled="!ipInput.trim() || isLoading"
|
|
class="btn-primary"
|
|
>
|
|
<FontAwesomeIcon
|
|
:icon="isLoading ? ['fas', 'spinner'] : ['fas', 'search']"
|
|
:class="['mr-2', isLoading && 'animate-spin']"
|
|
/>
|
|
{{ t('tools.ip_lookup.query') }}
|
|
</button>
|
|
|
|
<button
|
|
@click="getMyIP"
|
|
:disabled="isLoading"
|
|
class="btn-secondary"
|
|
>
|
|
<FontAwesomeIcon :icon="['fas', 'globe']" class="mr-2" />
|
|
{{ t('tools.ip_lookup.get_my_ip') }}
|
|
</button>
|
|
|
|
<button
|
|
@click="clearResults"
|
|
class="btn-secondary"
|
|
>
|
|
<FontAwesomeIcon :icon="['fas', 'trash']" class="mr-2" />
|
|
{{ t('tools.ip_lookup.clear') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<!-- 输入区域 -->
|
|
<div class="space-y-4">
|
|
<div class="card p-4">
|
|
<h3 class="text-lg font-semibold text-primary mb-3">{{ t('tools.ip_lookup.ip_input') }}</h3>
|
|
<input
|
|
v-model="ipInput"
|
|
type="text"
|
|
:placeholder="t('tools.ip_lookup.placeholder')"
|
|
class="input-field"
|
|
@keyup.enter="queryIP"
|
|
@input="validateIP"
|
|
>
|
|
<div v-if="ipValidation.message" class="mt-2 text-sm" :class="ipValidation.isValid ? 'text-success' : 'text-error'">
|
|
{{ ipValidation.message }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 常用IP -->
|
|
<div class="card p-4">
|
|
<h3 class="text-lg font-semibold text-primary mb-3">{{ t('tools.ip_lookup.common_ips') }}</h3>
|
|
<div class="space-y-2">
|
|
<button
|
|
v-for="ip in commonIPs"
|
|
:key="ip.ip"
|
|
@click="selectCommonIP(ip.ip)"
|
|
class="w-full text-left p-2 rounded bg-block hover:bg-block-hover text-secondary hover:text-primary transition-colors"
|
|
>
|
|
<div class="font-medium">{{ ip.ip }}</div>
|
|
<div class="text-sm text-tertiary">{{ ip.description }}</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 结果区域 -->
|
|
<div class="space-y-4">
|
|
<!-- 加载状态 -->
|
|
<div v-if="isLoading" class="card p-4">
|
|
<div class="text-center py-8">
|
|
<FontAwesomeIcon :icon="['fas', 'spinner']" class="text-4xl text-primary animate-spin mb-4" />
|
|
<div class="text-secondary">{{ t('tools.ip_lookup.querying') }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- IP信息结果 -->
|
|
<div v-else-if="ipResult" class="card p-4">
|
|
<h3 class="text-lg font-semibold text-primary mb-3">{{ t('tools.ip_lookup.ip_info') }}</h3>
|
|
|
|
<div class="space-y-3">
|
|
<div class="flex justify-between">
|
|
<span class="text-secondary">{{ t('tools.ip_lookup.ip_address') }}:</span>
|
|
<span class="text-primary font-medium">{{ ipResult.ip }}</span>
|
|
</div>
|
|
|
|
<div v-if="ipResult.type" class="flex justify-between">
|
|
<span class="text-secondary">{{ t('tools.ip_lookup.ip_type') }}:</span>
|
|
<span class="text-primary font-medium">{{ ipResult.type }}</span>
|
|
</div>
|
|
|
|
<div v-if="ipResult.country" class="flex justify-between">
|
|
<span class="text-secondary">{{ t('tools.ip_lookup.country') }}:</span>
|
|
<span class="text-primary font-medium">{{ ipResult.country }}</span>
|
|
</div>
|
|
|
|
<div v-if="ipResult.region" class="flex justify-between">
|
|
<span class="text-secondary">{{ t('tools.ip_lookup.region') }}:</span>
|
|
<span class="text-primary font-medium">{{ ipResult.region }}</span>
|
|
</div>
|
|
|
|
<div v-if="ipResult.city" class="flex justify-between">
|
|
<span class="text-secondary">{{ t('tools.ip_lookup.city') }}:</span>
|
|
<span class="text-primary font-medium">{{ ipResult.city }}</span>
|
|
</div>
|
|
|
|
<div v-if="ipResult.isp" class="flex justify-between">
|
|
<span class="text-secondary">{{ t('tools.ip_lookup.isp') }}:</span>
|
|
<span class="text-primary font-medium">{{ ipResult.isp }}</span>
|
|
</div>
|
|
|
|
<div v-if="ipResult.org" class="flex justify-between">
|
|
<span class="text-secondary">{{ t('tools.ip_lookup.organization') }}:</span>
|
|
<span class="text-primary font-medium">{{ ipResult.org }}</span>
|
|
</div>
|
|
|
|
<div v-if="ipResult.timezone" class="flex justify-between">
|
|
<span class="text-secondary">{{ t('tools.ip_lookup.timezone') }}:</span>
|
|
<span class="text-primary font-medium">{{ ipResult.timezone }}</span>
|
|
</div>
|
|
|
|
<div v-if="ipResult.lat && ipResult.lon" class="flex justify-between">
|
|
<span class="text-secondary">{{ t('tools.ip_lookup.coordinates') }}:</span>
|
|
<span class="text-primary font-medium">{{ ipResult.lat }}, {{ ipResult.lon }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 当前IP信息 -->
|
|
<div v-if="currentIP" class="card p-4">
|
|
<h3 class="text-lg font-semibold text-primary mb-3">{{ t('tools.ip_lookup.current_ip') }}</h3>
|
|
|
|
<div class="space-y-3">
|
|
<div class="flex justify-between">
|
|
<span class="text-secondary">{{ t('tools.ip_lookup.ip_address') }}:</span>
|
|
<span class="text-primary font-medium">{{ currentIP.ip }}</span>
|
|
</div>
|
|
|
|
<div v-if="currentIP.location" class="flex justify-between">
|
|
<span class="text-secondary">{{ t('tools.ip_lookup.location') }}:</span>
|
|
<span class="text-primary font-medium">{{ currentIP.location }}</span>
|
|
</div>
|
|
|
|
<div v-if="currentIP.isp" class="flex justify-between">
|
|
<span class="text-secondary">{{ t('tools.ip_lookup.isp') }}:</span>
|
|
<span class="text-primary font-medium">{{ currentIP.isp }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- IP类型检测 -->
|
|
<div v-if="ipInput.trim()" class="card p-4">
|
|
<h3 class="text-lg font-semibold text-primary mb-3">{{ t('tools.ip_lookup.ip_analysis') }}</h3>
|
|
|
|
<div class="space-y-3">
|
|
<div class="flex justify-between">
|
|
<span class="text-secondary">{{ t('tools.ip_lookup.format') }}:</span>
|
|
<span class="text-primary font-medium">{{ getIPFormat(ipInput) }}</span>
|
|
</div>
|
|
|
|
<div class="flex justify-between">
|
|
<span class="text-secondary">{{ t('tools.ip_lookup.access_type') }}:</span>
|
|
<span class="text-primary font-medium">{{ getIPAccessType(ipInput) }}</span>
|
|
</div>
|
|
|
|
<div v-if="isIPv4(ipInput)" class="flex justify-between">
|
|
<span class="text-secondary">{{ t('tools.ip_lookup.class') }}:</span>
|
|
<span class="text-primary font-medium">{{ getIPClass(ipInput) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 状态消息 -->
|
|
<div v-if="statusMessage" class="card p-4">
|
|
<div :class="[
|
|
'flex items-center space-x-2',
|
|
statusType === 'success' ? 'text-success' : 'text-error'
|
|
]">
|
|
<FontAwesomeIcon
|
|
:icon="statusType === 'success' ? ['fas', 'check'] : ['fas', 'times']"
|
|
/>
|
|
<span>{{ statusMessage }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
import { useLanguage } from '@/composables/useLanguage'
|
|
import { api } from '@/utils/api'
|
|
|
|
const { t } = useLanguage()
|
|
|
|
// 响应式状态
|
|
const ipInput = ref('')
|
|
const isLoading = ref(false)
|
|
const statusMessage = ref('')
|
|
const statusType = ref<'success' | 'error'>('success')
|
|
|
|
// IP验证状态
|
|
const ipValidation = ref({
|
|
isValid: false,
|
|
message: ''
|
|
})
|
|
|
|
// 查询结果
|
|
const ipResult = ref<{
|
|
ip: string
|
|
type?: string
|
|
country?: string
|
|
region?: string
|
|
city?: string
|
|
isp?: string
|
|
org?: string
|
|
timezone?: string
|
|
lat?: number
|
|
lon?: number
|
|
} | null>(null)
|
|
|
|
// 当前IP信息
|
|
const currentIP = ref<{
|
|
ip: string
|
|
location?: string
|
|
isp?: string
|
|
} | null>(null)
|
|
|
|
// 常用IP列表
|
|
const commonIPs = [
|
|
{ ip: '8.8.8.8', description: 'Google DNS' },
|
|
{ ip: '1.1.1.1', description: 'Cloudflare DNS' },
|
|
{ ip: '114.114.114.114', description: '114 DNS' },
|
|
{ ip: '223.5.5.5', description: '阿里 DNS' },
|
|
{ ip: '180.76.76.76', description: '百度 DNS' }
|
|
]
|
|
|
|
// 查询IP信息
|
|
const queryIP = async () => {
|
|
if (!ipInput.value.trim()) {
|
|
showStatus('请输入IP地址', 'error')
|
|
return
|
|
}
|
|
|
|
if (!ipValidation.value.isValid) {
|
|
showStatus('请输入有效的IP地址', 'error')
|
|
return
|
|
}
|
|
|
|
isLoading.value = true
|
|
statusMessage.value = ''
|
|
|
|
try {
|
|
// 使用免费的IP查询API
|
|
const response = await fetch(`http://ip-api.com/json/${ipInput.value}?lang=zh-CN`)
|
|
const data = await response.json()
|
|
|
|
if (data.status === 'success') {
|
|
ipResult.value = {
|
|
ip: data.query,
|
|
type: isIPv4(data.query) ? 'IPv4' : 'IPv6',
|
|
country: data.country,
|
|
region: data.regionName,
|
|
city: data.city,
|
|
isp: data.isp,
|
|
org: data.org,
|
|
timezone: data.timezone,
|
|
lat: data.lat,
|
|
lon: data.lon
|
|
}
|
|
showStatus('IP查询成功', 'success')
|
|
} else {
|
|
throw new Error(data.message || 'IP查询失败')
|
|
}
|
|
} catch (error) {
|
|
console.error('IP查询失败:', error)
|
|
showStatus('IP查询失败: ' + (error instanceof Error ? error.message : '网络错误'), 'error')
|
|
ipResult.value = null
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
// 获取当前IP
|
|
const getMyIP = async () => {
|
|
isLoading.value = true
|
|
statusMessage.value = ''
|
|
|
|
try {
|
|
// 首先尝试获取当前IP
|
|
const ipResponse = await fetch('https://api.ipify.org?format=json')
|
|
const ipData = await ipResponse.json()
|
|
|
|
// 然后查询IP详细信息
|
|
const detailResponse = await fetch(`http://ip-api.com/json/${ipData.ip}?lang=zh-CN`)
|
|
const detailData = await detailResponse.json()
|
|
|
|
if (detailData.status === 'success') {
|
|
currentIP.value = {
|
|
ip: ipData.ip,
|
|
location: `${detailData.country} ${detailData.regionName} ${detailData.city}`,
|
|
isp: detailData.isp
|
|
}
|
|
|
|
// 同时设置到输入框
|
|
ipInput.value = ipData.ip
|
|
validateIP()
|
|
|
|
showStatus('当前IP获取成功', 'success')
|
|
} else {
|
|
throw new Error('获取IP详细信息失败')
|
|
}
|
|
} catch (error) {
|
|
console.error('获取当前IP失败:', error)
|
|
showStatus('获取当前IP失败: ' + (error instanceof Error ? error.message : '网络错误'), 'error')
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
// 选择常用IP
|
|
const selectCommonIP = (ip: string) => {
|
|
ipInput.value = ip
|
|
validateIP()
|
|
}
|
|
|
|
// 清除结果
|
|
const clearResults = () => {
|
|
ipInput.value = ''
|
|
ipResult.value = null
|
|
currentIP.value = null
|
|
statusMessage.value = ''
|
|
ipValidation.value = { isValid: false, message: '' }
|
|
}
|
|
|
|
// 验证IP地址
|
|
const validateIP = () => {
|
|
const ip = ipInput.value.trim()
|
|
|
|
if (!ip) {
|
|
ipValidation.value = { isValid: false, message: '' }
|
|
return
|
|
}
|
|
|
|
if (isIPv4(ip)) {
|
|
ipValidation.value = { isValid: true, message: '有效的IPv4地址' }
|
|
} else if (isIPv6(ip)) {
|
|
ipValidation.value = { isValid: true, message: '有效的IPv6地址' }
|
|
} else {
|
|
ipValidation.value = { isValid: false, message: '无效的IP地址格式' }
|
|
}
|
|
}
|
|
|
|
// 检查是否为IPv4
|
|
const isIPv4 = (ip: string): boolean => {
|
|
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
|
|
return ipv4Regex.test(ip)
|
|
}
|
|
|
|
// 检查是否为IPv6
|
|
const isIPv6 = (ip: string): boolean => {
|
|
const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/
|
|
return ipv6Regex.test(ip)
|
|
}
|
|
|
|
// 获取IP格式
|
|
const getIPFormat = (ip: string): string => {
|
|
if (isIPv4(ip)) return 'IPv4'
|
|
if (isIPv6(ip)) return 'IPv6'
|
|
return '无效格式'
|
|
}
|
|
|
|
// 获取IP访问类型
|
|
const getIPAccessType = (ip: string): string => {
|
|
if (!isIPv4(ip)) return '未知'
|
|
|
|
const parts = ip.split('.').map(Number)
|
|
const first = parts[0]
|
|
const second = parts[1]
|
|
|
|
// 私有IP地址
|
|
if (first === 10) return '私有网络 (Class A)'
|
|
if (first === 172 && second >= 16 && second <= 31) return '私有网络 (Class B)'
|
|
if (first === 192 && second === 168) return '私有网络 (Class C)'
|
|
if (first === 127) return '本地回环'
|
|
if (first === 169 && second === 254) return '链路本地'
|
|
|
|
return '公网'
|
|
}
|
|
|
|
// 获取IP类别 (仅IPv4)
|
|
const getIPClass = (ip: string): string => {
|
|
if (!isIPv4(ip)) return ''
|
|
|
|
const first = parseInt(ip.split('.')[0])
|
|
|
|
if (first >= 1 && first <= 126) return 'A类 (1-126)'
|
|
if (first >= 128 && first <= 191) return 'B类 (128-191)'
|
|
if (first >= 192 && first <= 223) return 'C类 (192-223)'
|
|
if (first >= 224 && first <= 239) return 'D类 (组播)'
|
|
if (first >= 240 && first <= 255) return 'E类 (保留)'
|
|
|
|
return '未知'
|
|
}
|
|
|
|
// 显示状态消息
|
|
const showStatus = (message: string, type: 'success' | 'error') => {
|
|
statusMessage.value = message
|
|
statusType.value = type
|
|
setTimeout(() => {
|
|
statusMessage.value = ''
|
|
}, 3000)
|
|
}
|
|
|
|
// 组件挂载时获取当前IP
|
|
onMounted(() => {
|
|
// 可以选择是否自动获取当前IP
|
|
// getMyIP()
|
|
})
|
|
</script> |