404 lines
13 KiB
Vue
404 lines
13 KiB
Vue
<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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''')
|
||
}
|
||
|
||
// 计算高亮文本
|
||
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> |