forked from zguiy/utils
		
	
		
			
				
	
	
		
			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>  |