工具完成
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing

This commit is contained in:
2025-06-28 22:38:49 +08:00
parent 2c668fedd0
commit 8400dbfab9
60 changed files with 23197 additions and 144 deletions

View File

@ -0,0 +1,341 @@
<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>