Files
utils/src/components/tools/UrlEncoder.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

341 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="space-y-6">
<!-- 工具栏 -->
<div class="card p-4">
<div class="flex flex-wrap gap-2">
<button
@click="() => convert('encode')"
class="btn-primary"
>
<FontAwesomeIcon :icon="['fas', 'lock']" class="mr-2" />
URL编码
</button>
<button
@click="() => convert('decode')"
class="btn-secondary"
>
<FontAwesomeIcon :icon="['fas', 'unlock']" class="mr-2" />
URL解码
</button>
<button
@click="() => convert('component-encode')"
class="btn-secondary"
>
<FontAwesomeIcon :icon="['fas', 'compress']" class="mr-2" />
组件编码
</button>
<button
@click="() => convert('component-decode')"
class="btn-secondary"
>
<FontAwesomeIcon :icon="['fas', 'expand']" class="mr-2" />
组件解码
</button>
<button
@click="clearAll"
class="btn-secondary"
>
<FontAwesomeIcon :icon="['fas', 'trash']" class="mr-2" />
清空
</button>
</div>
</div>
<!-- 输入输出区域 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 输入区域 -->
<div class="card p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="text-lg font-semibold text-primary">输入</h3>
<button
@click="pasteFromClipboard"
class="p-2 rounded text-secondary hover:text-primary transition-colors"
title="粘贴"
>
<FontAwesomeIcon :icon="['fas', 'clipboard']" />
</button>
</div>
<textarea
v-model="inputText"
placeholder="输入要编码或解码的URL或文本..."
class="textarea-field h-80"
/>
<div class="mt-3 text-sm text-tertiary">
<p>字符数量: {{ inputText.length }}</p>
</div>
</div>
<!-- 输出区域 -->
<div class="card p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="text-lg font-semibold text-primary">输出</h3>
<button
@click="copyToClipboard"
:disabled="!outputText"
class="p-2 rounded text-secondary hover:text-primary transition-colors disabled:opacity-50"
title="复制"
>
<FontAwesomeIcon
:icon="copied ? ['fas', 'check'] : ['fas', 'copy']"
:class="copied && 'text-success'"
/>
</button>
</div>
<textarea
v-model="outputText"
placeholder="转换结果将显示在这里..."
class="textarea-field h-80"
readonly
/>
</div>
</div>
<!-- URL 分析 -->
<div v-if="urlParts" class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">URL 分析</h3>
<div class="space-y-3">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-if="urlParts.protocol" class="bg-block p-3 rounded">
<div class="text-sm font-medium text-secondary mb-1">协议</div>
<div class="font-mono text-sm">{{ urlParts.protocol }}</div>
</div>
<div v-if="urlParts.hostname" class="bg-block p-3 rounded">
<div class="text-sm font-medium text-secondary mb-1">主机名</div>
<div class="font-mono text-sm">{{ urlParts.hostname }}</div>
</div>
<div v-if="urlParts.port" class="bg-block p-3 rounded">
<div class="text-sm font-medium text-secondary mb-1">端口</div>
<div class="font-mono text-sm">{{ urlParts.port }}</div>
</div>
<div v-if="urlParts.pathname" class="bg-block p-3 rounded">
<div class="text-sm font-medium text-secondary mb-1">路径</div>
<div class="font-mono text-sm">{{ urlParts.pathname }}</div>
</div>
<div v-if="urlParts.search" class="bg-block p-3 rounded">
<div class="text-sm font-medium text-secondary mb-1">查询参数</div>
<div class="font-mono text-sm">{{ urlParts.search }}</div>
</div>
<div v-if="urlParts.hash" class="bg-block p-3 rounded">
<div class="text-sm font-medium text-secondary mb-1">锚点</div>
<div class="font-mono text-sm">{{ urlParts.hash }}</div>
</div>
</div>
<div v-if="queryParams.length > 0" class="mt-4">
<h4 class="text-lg font-semibold text-primary mb-2">查询参数详情</h4>
<div class="space-y-2">
<div
v-for="(param, index) in queryParams"
:key="index"
class="bg-block p-3 rounded flex justify-between"
>
<div class="font-mono text-sm">
<span class="text-primary">{{ param.key }}</span>
<span class="text-secondary mx-2">=</span>
<span class="text-secondary">{{ param.value }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 编码对照表 -->
<div class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">常用字符编码对照表</h3>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
<div
v-for="char in commonChars"
:key="char.original"
class="bg-block p-3 rounded text-center"
>
<div class="text-lg font-bold text-primary">{{ char.original }}</div>
<div class="text-sm text-secondary font-mono">{{ char.encoded }}</div>
</div>
</div>
</div>
<!-- 快速示例 -->
<div class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">快速示例</h3>
<div class="space-y-3">
<div
v-for="example in examples"
:key="example.name"
class="bg-block p-3 rounded"
>
<div class="flex items-center justify-between mb-2">
<h4 class="font-medium text-secondary">{{ example.name }}</h4>
<button
@click="() => useExample(example.url)"
class="btn-secondary text-sm"
>
使用此示例
</button>
</div>
<div class="font-mono text-sm break-all">{{ example.url }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
// 响应式状态
const inputText = ref('')
const outputText = ref('')
const copied = ref(false)
const urlParts = ref<any>(null)
const queryParams = ref<Array<{ key: string, value: string }>>([])
// 常用字符编码对照
const commonChars = [
{ original: ' ', encoded: '%20' },
{ original: '!', encoded: '%21' },
{ original: '#', encoded: '%23' },
{ original: '$', encoded: '%24' },
{ original: '&', encoded: '%26' },
{ original: "'", encoded: '%27' },
{ original: '(', encoded: '%28' },
{ original: ')', encoded: '%29' },
{ original: '+', encoded: '%2B' },
{ original: ',', encoded: '%2C' },
{ original: '/', encoded: '%2F' },
{ original: ':', encoded: '%3A' },
{ original: ';', encoded: '%3B' },
{ original: '=', encoded: '%3D' },
{ original: '?', encoded: '%3F' },
{ original: '@', encoded: '%40' }
]
// 示例URL
const examples = [
{
name: 'Google搜索',
url: 'https://www.google.com/search?q=URL编码&hl=zh-CN'
},
{
name: '包含中文的URL',
url: 'https://example.com/用户/信息?姓名=张三&年龄=25'
},
{
name: '包含特殊字符',
url: 'https://api.example.com/data?filter=name eq "John Doe"&sort=created_at desc'
},
{
name: '已编码的URL',
url: 'https://example.com/%E7%94%A8%E6%88%B7?name=%E5%BC%A0%E4%B8%89'
}
]
// 转换函数
const convert = (type: 'encode' | 'decode' | 'component-encode' | 'component-decode') => {
if (!inputText.value.trim()) return
try {
switch (type) {
case 'encode':
outputText.value = encodeURI(inputText.value)
break
case 'decode':
outputText.value = decodeURI(inputText.value)
break
case 'component-encode':
outputText.value = encodeURIComponent(inputText.value)
break
case 'component-decode':
outputText.value = decodeURIComponent(inputText.value)
break
}
} catch (error) {
outputText.value = '转换失败: ' + (error instanceof Error ? error.message : '未知错误')
}
}
// 分析URL结构
const analyzeURL = (url: string) => {
try {
const urlObj = new URL(url)
urlParts.value = {
protocol: urlObj.protocol,
hostname: urlObj.hostname,
port: urlObj.port || '默认端口',
pathname: urlObj.pathname,
search: urlObj.search,
hash: urlObj.hash
}
// 解析查询参数
queryParams.value = []
urlObj.searchParams.forEach((value, key) => {
queryParams.value.push({ key, value })
})
} catch (error) {
urlParts.value = null
queryParams.value = []
}
}
// 使用示例
const useExample = (url: string) => {
inputText.value = url
}
// 清空所有内容
const clearAll = () => {
inputText.value = ''
outputText.value = ''
urlParts.value = null
queryParams.value = []
}
// 粘贴功能
const pasteFromClipboard = async () => {
try {
const text = await navigator.clipboard.readText()
inputText.value = text
} catch (error) {
console.error('粘贴失败:', error)
}
}
// 复制功能
const copyToClipboard = async () => {
if (!outputText.value) return
try {
await navigator.clipboard.writeText(outputText.value)
copied.value = true
setTimeout(() => {
copied.value = false
}, 2000)
} catch (error) {
console.error('复制失败:', error)
}
}
// 监听输入变化分析URL
watch(inputText, (newValue) => {
if (newValue.trim()) {
analyzeURL(newValue)
} else {
urlParts.value = null
queryParams.value = []
}
}, { immediate: true })
</script>