341 lines
10 KiB
Vue
341 lines
10 KiB
Vue
<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> |