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

404 lines
13 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="grid grid-cols-1 lg:grid-cols-4 gap-6">
<!-- 左侧面板 - 常用示例和选项 -->
<div class="lg:col-span-1 space-y-6">
<!-- 常用示例 -->
<div class="card p-4">
<h2 class="text-md font-medium text-primary mb-4">常用示例</h2>
<div class="space-y-2">
<button
v-for="(example, index) in examples"
:key="index"
class="text-left w-full px-3 py-2 rounded-md text-sm text-secondary hover:bg-hover transition-colors"
@click="() => applyExample(example)"
>
{{ example.name }}
</button>
</div>
</div>
<!-- 正则选项 -->
<div class="card p-4">
<div class="flex items-center justify-between mb-4">
<h2 class="text-md font-medium text-primary">选项</h2>
<button
class="text-tertiary hover:text-error transition-colors text-sm"
@click="clearAll"
>
<FontAwesomeIcon :icon="['fas', 'trash']" class="mr-1" />
清空
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm text-secondary mb-2">标志位</label>
<div class="flex flex-wrap gap-2">
<button
:class="flags.includes('g') ? 'btn-primary text-xs' : 'btn-secondary text-xs'"
@click="() => toggleFlag('g')"
>
g (全局)
</button>
<button
:class="flags.includes('i') ? 'btn-primary text-xs' : 'btn-secondary text-xs'"
@click="() => toggleFlag('i')"
>
i (忽略大小写)
</button>
<button
:class="flags.includes('m') ? 'btn-primary text-xs' : 'btn-secondary text-xs'"
@click="() => toggleFlag('m')"
>
m (多行)
</button>
<button
:class="flags.includes('s') ? 'btn-primary text-xs' : 'btn-secondary text-xs'"
@click="() => toggleFlag('s')"
>
s (单行)
</button>
</div>
</div>
<div>
<label class="flex items-center text-sm text-secondary">
<input
v-model="showGroups"
type="checkbox"
class="mr-2"
/>
显示捕获组
</label>
</div>
</div>
</div>
</div>
<!-- 右侧面板 - 测试区域 -->
<div class="lg:col-span-3 space-y-6">
<!-- 正则表达式输入 -->
<div class="card p-4">
<div class="flex items-center justify-between mb-4">
<h2 class="text-md font-medium text-primary">正则表达式</h2>
<button
class="text-tertiary hover:text-primary transition-colors text-sm"
@click="copyRegex"
:disabled="!regexString"
>
<FontAwesomeIcon :icon="copied ? ['fas', 'check'] : ['fas', 'copy']" class="mr-1" />
{{ copied ? '已复制' : '复制' }}
</button>
</div>
<div class="relative">
<div class="absolute left-3 top-[13px] text-tertiary">/</div>
<input
v-model="regexString"
type="text"
placeholder="输入正则表达式..."
class="input-field pl-7 pr-14"
/>
<div class="absolute right-14 top-[13px] text-tertiary">/</div>
<input
v-model="flags"
type="text"
placeholder="flags"
class="absolute right-3 top-[13px] w-8 bg-transparent border-none outline-none text-tertiary"
/>
</div>
<div v-if="regexError" class="mt-2 text-sm text-error flex items-center gap-2">
<FontAwesomeIcon :icon="['fas', 'exclamation-triangle']" />
<span>{{ regexError }}</span>
</div>
</div>
<!-- 测试输入 -->
<div class="card p-4">
<div class="flex items-center justify-between mb-4">
<h2 class="text-md font-medium text-primary">测试文本</h2>
<div class="text-sm text-secondary">
字符数: <span class="text-primary">{{ testString.length }}</span>
</div>
</div>
<textarea
v-model="testString"
placeholder="输入要测试的文本..."
class="textarea-field min-h-[150px] w-full resize-y"
/>
</div>
<!-- 匹配结果 -->
<div class="card p-4">
<div class="flex items-center justify-between mb-4">
<h2 class="text-md font-medium text-primary">匹配结果</h2>
<div class="text-sm text-secondary">
匹配数量: <span class="text-primary">{{ matchCount }}</span>
</div>
</div>
<div v-if="testString" class="space-y-4">
<!-- 高亮显示的匹配文本 -->
<div class="bg-block rounded-md p-4 whitespace-pre-wrap font-mono text-sm">
<div v-if="matchCount > 0">
<div class="mb-3 text-tertiary text-xs flex items-center justify-between">
<span>
找到 <span class="text-primary font-medium">{{ matchCount }}</span> 个匹配项
</span>
<span class="text-tertiary text-xs">
原文长度: {{ testString.length }} 字符
</span>
</div>
<!-- 使用 v-html 显示高亮结果但要确保安全 -->
<div v-html="highlightedText" class="break-all"></div>
</div>
<span v-else class="text-tertiary">无匹配项</span>
</div>
<!-- 捕获组详情 -->
<div v-if="showGroups && matchCount > 0">
<h3 class="text-sm font-medium text-primary mb-2">捕获组详情</h3>
<div class="space-y-2">
<div
v-for="(match, index) in matches"
:key="index"
class="bg-block rounded-md p-3"
>
<div class="text-xs text-tertiary mb-2">
匹配 #{{ index + 1 }} (位置: {{ match.index }})
</div>
<div class="space-y-1">
<div class="flex items-start gap-2">
<span class="text-xs text-tertiary min-w-[40px]">完整:</span>
<code class="text-sm text-primary bg-primary/10 px-1 rounded break-all">
{{ match[0] || '' }}
</code>
</div>
<div
v-for="group in Math.max(0, match.length - 1)"
:key="group"
class="flex items-start gap-2"
>
<span class="text-xs text-tertiary min-w-[40px]"> {{ group }}:</span>
<code class="text-sm text-primary bg-primary/10 px-1 rounded break-all">
{{ match[group] || '(空)' }}
</code>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else class="flex items-center justify-center p-4 text-tertiary">
<FontAwesomeIcon :icon="['fas', 'info-circle']" class="mr-2" />
请输入测试文本
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
// 定义匹配结果类型
interface MatchResult extends RegExpExecArray {
index: number
}
// 响应式状态
const regexString = ref('')
const flags = ref('g')
const testString = ref('')
const matches = ref<MatchResult[]>([])
const matchCount = ref(0)
const showGroups = ref(true)
const regexError = ref<string | null>(null)
const copied = ref(false)
// 常用正则表达式示例
const examples = [
{
name: '邮箱地址',
pattern: '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}',
flags: 'g',
testText: 'test@example.com, invalid-email, another.email@domain.co.uk'
},
{
name: '手机号码',
pattern: '1[3-9]\\d{9}',
flags: 'g',
testText: '我的手机号是13812345678她的是15987654321座机010-12345678'
},
{
name: 'URL地址',
pattern: 'https?://[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&//=]*)',
flags: 'g',
testText: '访问 https://www.example.com 或 http://test.org/path?query=1'
},
{
name: 'IP地址',
pattern: '\\b(?:(?: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]?)\\b',
flags: 'g',
testText: '服务器IP: 192.168.1.1, 公网IP: 8.8.8.8, 错误格式: 999.999.999.999'
},
{
name: '中文字符',
pattern: '[\\u4e00-\\u9fa5]',
flags: 'g',
testText: 'Hello 世界! This is 中文 mixed with English.'
}
]
// HTML转义函数
const escapeHtml = (text: string): string => {
if (!text) return ''
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
// 计算高亮文本
const highlightedText = computed(() => {
if (!testString.value || matchCount.value === 0) {
return escapeHtml(testString.value)
}
let result = ''
let lastIndex = 0
// 按索引顺序排序匹配项
const sortedMatches = [...matches.value].sort((a, b) => a.index - b.index)
// 遍历每个匹配项
sortedMatches.forEach(match => {
// 添加匹配前的文本
result += escapeHtml(testString.value.substring(lastIndex, match.index))
// 添加高亮的匹配内容
result += `<span style="background-color:rgba(var(--color-primary), 0.3); color:rgb(var(--color-primary)); font-weight:bold; padding:0 4px; border-radius:3px;">${escapeHtml(match[0])}</span>`
// 更新lastIndex
lastIndex = match.index + match[0].length
})
// 添加最后一个匹配后的文本
if (lastIndex < testString.value.length) {
result += escapeHtml(testString.value.substring(lastIndex))
}
return result
})
// 测试正则表达式
const testRegex = () => {
if (!regexString.value || !testString.value) {
matches.value = []
matchCount.value = 0
regexError.value = null
return
}
try {
// 验证正则表达式是否有效
new RegExp(regexString.value, flags.value)
regexError.value = null
if (flags.value.includes('g')) {
// 获取所有匹配
const allMatches: MatchResult[] = []
let match: RegExpExecArray | null
const regexWithGroups = new RegExp(regexString.value, flags.value)
// 收集所有匹配和捕获组
while ((match = regexWithGroups.exec(testString.value)) !== null) {
allMatches.push(match as MatchResult)
// 防止无限循环如果匹配长度为0手动增加索引
if (match.index === regexWithGroups.lastIndex) {
regexWithGroups.lastIndex++
}
}
matches.value = allMatches
matchCount.value = allMatches.length
} else {
// 单次匹配模式
const regexWithoutG = new RegExp(regexString.value, flags.value.replace('g', ''))
const execMatch = regexWithoutG.exec(testString.value)
if (execMatch) {
matches.value = [execMatch as MatchResult]
matchCount.value = 1
} else {
matches.value = []
matchCount.value = 0
}
}
} catch (error) {
console.error('正则表达式错误:', error)
regexError.value = (error as Error).message
matches.value = []
matchCount.value = 0
}
}
// 复制正则表达式
const copyRegex = async () => {
if (!regexString.value) return
try {
const regexText = `/${regexString.value}/${flags.value}`
await navigator.clipboard.writeText(regexText)
copied.value = true
setTimeout(() => {
copied.value = false
}, 2000)
} catch (error) {
console.error('复制失败:', error)
}
}
// 应用示例
const applyExample = (example: { pattern: string; flags: string; testText: string }) => {
regexString.value = example.pattern
flags.value = example.flags
testString.value = example.testText
}
// 清空所有内容
const clearAll = () => {
regexString.value = ''
flags.value = 'g'
testString.value = ''
matches.value = []
matchCount.value = 0
regexError.value = null
}
// 切换标志位
const toggleFlag = (flag: string) => {
if (flags.value.includes(flag)) {
flags.value = flags.value.replace(flag, '')
} else {
flags.value = flags.value + flag
}
}
// 监听输入变化,自动测试
watch([regexString, flags, testString, showGroups], () => {
testRegex()
}, { immediate: true })
</script>