Files
utils/src/components/tools/IpLookup.vue
zguiy 8400dbfab9
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
工具完成
2025-06-28 22:38:49 +08:00

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>