用 Python 揭秘 IP 地址背后的地理位置和信息
2025年10月20日 22:13
准备工作:安装必备工具 首先,请确保你的Python环境中安装了requests库。 第一步:查询自己的公网 IP 信息 运行后,你将看到类似这样的输出(信息会根据你的实际情况而变): 第二步:查询任
效果图
使用
<template>
<Button @click="openDrawer1">从右边显示抽屉</Button>
<Button @click="openDrawer2">从左边显示抽屉</Button>
<Button @click="openDrawer3">从上边显示抽屉</Button>
<Button @click="openDrawer4">从下边显示抽屉</Button>
<Drawer v-model="showDrawer1" direction="right" title="抽屉组件"></Drawer>
<Drawer v-model="showDrawer2" direction="left" title="抽屉组件"></Drawer>
<Drawer v-model="showDrawer3" direction="top" title="抽屉组件"></Drawer>
<Drawer v-model="showDrawer4" direction="bottom" title="抽屉组件"></Drawer>
</template>
<script lang="ts" setup>
const showDrawer1 = ref(false);
const showDrawer2 = ref(false);
const showDrawer3 = ref(false);
const showDrawer4 = ref(false);
const openDrawer1 = () => {
showDrawer1.value = true
}
const openDrawer2 = () => {
showDrawer2.value = true
}
const openDrawer3 = () => {
showDrawer3.value = true
}
const openDrawer4 = () => {
showDrawer4.value = true
}
</script>
|-Drawer
|--Drawer.vue(组件)
|--style.css(样式)
|--types.ts(类型定义)
|--index.ts(入口文件)
<template>
<Overlay :modelValue="modelValue" :modalClass="modalClass">
<Transition :name="transitionName">
<div
ref="drawerRef"
class="vh-drawer"
:class="[`vh-drawer--${direction}`, customClass]"
:style="drawerStyle"
v-if="modelValue"
>
<div class="vh-drawer__header" v-if="title || $slots.header">
<slot name="header">{{ title }}</slot>
<Icon class="vh-drawer__close" icon="xmark" v-if="showClose" @click.stop="handleClose" />
</div>
<div class="vh-drawer__body">
<slot />
</div>
<div class="vh-drawer__footer" v-if="$slots.footer">
<slot name="footer" />
</div>
</div>
</Transition>
</Overlay>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick, onBeforeUnmount } from 'vue'
import Icon from '../Icon/Icon.vue'
import Overlay from '../Overlay/Overlay.vue'
import type { DrawerProps, DrawerEmits } from './types'
import { isFunction } from 'lodash-es'
defineOptions({ name: 'vhDrawer' })
const props = withDefaults(defineProps<DrawerProps>(), {
direction: 'right',
size: '30%',
title: '',
showClose: true,
closeOnClickModal: true,
closeOnPressEscape: true,
beforeClose: undefined,
customClass: '',
modal: true,
modalClass: '',
openDelay: 0,
closeDelay: 0
})
const emits = defineEmits<DrawerEmits>()
const drawerRef = ref<HTMLElement>()
const isVisible = ref(false)
// 计算抽屉样式
const drawerStyle = computed(() => {
const style: Record<string, string> = {}
const isHorizontal = ['left', 'right'].includes(props.direction)
if (isHorizontal) {
style.width = typeof props.size === 'number' ? `${props.size}px` : props.size
} else {
style.height = typeof props.size === 'number' ? `${props.size}px` : props.size
}
return style
})
// 根据方向计算过渡动画名称
const transitionName = computed(() => {
return `vh-drawer-${props.direction}`
})
// 关闭抽屉
const handleClose = () => {
if (isFunction(props.beforeClose)) {
props.beforeClose(() => {
emits('update:modelValue', false)
})
} else {
emits('update:modelValue', false)
}
}
// 处理点击外部关闭
const handleWrapperClick = (e: MouseEvent) => {
if (e.target === e.currentTarget && props.closeOnClickModal) {
handleClose()
}
}
// 处理ESC键关闭
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && props.closeOnPressEscape) {
handleClose()
}
}
// 过渡动画钩子
const handleBeforeEnter = () => {
isVisible.value = true
emits('open')
}
const handleAfterEnter = () => {
emits('opened')
}
const handleBeforeLeave = () => {
emits('close')
}
const handleAfterLeave = () => {
isVisible.value = false
emits('closed')
}
// 监听显示状态
watch(
() => props.modelValue,
(newVal) => {
if (newVal) {
nextTick(() => {
document.addEventListener('keydown', handleKeydown)
})
} else {
document.removeEventListener('keydown', handleKeydown)
}
}
)
// 组件卸载时清理事件监听
onBeforeUnmount(() => {
document.removeEventListener('keydown', handleKeydown)
})
</script>
import type { PropType } from 'vue'
export type PaginationLayout = string
export interface PaginationProps {
// 当前页码
pageNum?: number
// 每页显示条数
pageSize?: number
// 总条数
total: number
// 每页显示条数选择器的选项
pageSizes?: number[]export interface DrawerProps {
modelValue: boolean;
direction?: 'left' | 'right' | 'top' | 'bottom';
size?: number | string;
title?: string;
showClose?: boolean;
closeOnClickModal?: boolean;
closeOnPressEscape?: boolean;
beforeClose?: (done: () => void) => void;
customClass?: string;
modal?: boolean;
modalClass?: string;
openDelay?: number;
closeDelay?: number;
}
export interface DrawerEmits {
(e: 'update:modelValue', value: boolean): void;
(e: 'open'): void;
(e: 'opened'): void;
(e: 'close'): void;
(e: 'closed'): void;
}
// 布局配置 (total, sizes, prev, pager, next, jumper)
layout?: PaginationLayout
// 背景是否显示
background?: boolean
// 是否禁用
disabled?: boolean
// 是否显示小型分页
small?: boolean
// 页码按钮的数量,当总页数超过该值时会折叠
pagerCount?: number
// 上一页按钮的文本
prevText?: string
// 下一页按钮的文本
nextText?: string
// 省略时显示的内容
ellipsisText?: string
// 自定义跳转内容
jumper?: boolean
// 自定义大小选择器的内容
sizeSelector?: boolean
// 自定义总条数显示的内容
totalSelector?: boolean
}
// 事件类型定义
export interface PaginationEmits {
(e: 'update:pageNum', value: number): void
(e: 'update:pageSize', value: number): void
(e: 'size-change', size: number): void
(e: 'current-change', current: number): void
(e: 'prev-click', current: number): void
(e: 'next-click', current: number): void
}
import type { App } from 'vue'
import Drawer from './Drawer.vue'
Drawer.install = (app: App) => {
app.component(Drawer.name, Drawer)
}
export default Drawer
export * from './types'
.vh-drawer__wrapper {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2000;
overflow: hidden;
}
.vh-drawer {
position: fixed;
background-color: var(--vh-bg-color);
transition: all var(--vh-transition-duration) ease;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
z-index: 2001;
&--left {
top: 0;
left: 0;
bottom: 0;
border-right: var(--vh-border);
}
&--right {
top: 0;
right: 0;
bottom: 0;
border-left: var(--vh-border);
}
&--top {
top: 0;
left: 0;
right: 0;
border-bottom: var(--vh-border);
}
&--bottom {
bottom: 0;
left: 0;
right: 0;
border-top: var(--vh-border);
}
}
.vh-drawer__header {
padding: 20px 24px;
border-bottom: var(--vh-border);
display: flex;
align-items: center;
justify-content: space-between;
font-size: var(--vh-font-size-large);
font-weight: var(--vh-font-weight-primary);
color: var(--vh-text-color-primary);
}
.vh-drawer__close {
color: var(--vh-text-color-regular);
font-size: var(--vh-font-size-large);
cursor: pointer;
transition: color var(--vh-transition-duration);
&:hover {
color: var(--vh-text-color-primary);
}
}
.vh-drawer__body {
flex: 1;
padding: 24px;
overflow-y: auto;
}
.vh-drawer__footer {
padding: 16px 24px;
border-top: var(--vh-border);
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
}
.vh-drawer-right-enter-from {
transform: translateX(100%);
}
.vh-drawer-right-leave-to {
transform: translateX(100%);
}
/* 左侧抽屉动画 */
.vh-drawer-left-enter-from {
transform: translateX(-100%);
}
.vh-drawer-left-leave-to {
transform: translateX(-100%);
}
/* 顶部抽屉动画 */
.vh-drawer-top-enter-from {
transform: translateY(-100%);
}
.vh-drawer-top-leave-to {
transform: translateY(-100%);
}
/* 底部抽屉动画 */
.vh-drawer-bottom-enter-from {
transform: translateY(100%);
}
.vh-drawer-bottom-leave-to {
transform: translateY(100%);
}
其他文章