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

711 lines
22 KiB
Vue

<template>
<div class="space-y-6">
<!-- 工具栏 -->
<div class="card p-4">
<div class="flex flex-wrap gap-2">
<button
@click="processText"
:disabled="!inputText.trim()"
class="btn-primary"
>
<FontAwesomeIcon :icon="['fas', 'magic']" class="mr-2" />
处理文本
</button>
<button
@click="copyResult"
:disabled="!outputText"
class="btn-secondary"
>
<FontAwesomeIcon
:icon="copied ? ['fas', 'check'] : ['fas', 'copy']"
:class="['mr-2', copied && 'text-success']"
/>
复制结果
</button>
<button
@click="clearAll"
class="btn-secondary"
>
<FontAwesomeIcon :icon="['fas', 'trash']" class="mr-2" />
清除
</button>
<button
@click="swapContent"
:disabled="!outputText"
class="btn-secondary"
>
<FontAwesomeIcon :icon="['fas', 'exchange-alt']" class="mr-2" />
交换内容
</button>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 输入区域 -->
<div class="space-y-4">
<!-- 输入文本 -->
<div class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">输入文本</h3>
<textarea
v-model="inputText"
placeholder="请输入需要处理的文本..."
class="textarea-field h-80 font-mono text-sm"
@input="updateStats"
/>
<!-- 输入统计 -->
<div class="flex justify-between items-center mt-2 text-sm text-secondary">
<span>{{ inputStats.chars }} 字符 | {{ inputStats.lines }} </span>
<span>{{ inputStats.spaces }} 空格 | {{ inputStats.tabs }} 制表符</span>
</div>
</div>
<!-- 处理选项 -->
<div class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">处理选项</h3>
<div class="space-y-4">
<!-- 空格处理 -->
<div>
<label class="block text-sm font-medium text-secondary mb-2">空格处理</label>
<div class="space-y-2">
<label class="flex items-center space-x-2">
<input
v-model="options.removeLeadingSpaces"
type="checkbox"
class="form-checkbox"
>
<span class="text-secondary">移除行首空格</span>
</label>
<label class="flex items-center space-x-2">
<input
v-model="options.removeTrailingSpaces"
type="checkbox"
class="form-checkbox"
>
<span class="text-secondary">移除行尾空格</span>
</label>
<label class="flex items-center space-x-2">
<input
v-model="options.removeAllSpaces"
type="checkbox"
class="form-checkbox"
>
<span class="text-secondary">移除所有空格</span>
</label>
<label class="flex items-center space-x-2">
<input
v-model="options.collapseSpaces"
type="checkbox"
class="form-checkbox"
>
<span class="text-secondary">合并连续空格</span>
</label>
</div>
</div>
<!-- 制表符处理 -->
<div>
<label class="block text-sm font-medium text-secondary mb-2">制表符处理</label>
<div class="space-y-2">
<label class="flex items-center space-x-2">
<input
v-model="options.removeTabs"
type="checkbox"
class="form-checkbox"
>
<span class="text-secondary">移除制表符</span>
</label>
<label class="flex items-center space-x-2">
<input
v-model="options.tabsToSpaces"
type="checkbox"
class="form-checkbox"
>
<span class="text-secondary">制表符转空格</span>
</label>
<div v-if="options.tabsToSpaces" class="ml-6">
<label class="block text-xs text-tertiary mb-1">空格数量</label>
<input
v-model.number="options.tabSize"
type="number"
min="1"
max="8"
class="input-field w-20 text-sm"
>
</div>
</div>
</div>
<!-- 换行处理 -->
<div>
<label class="block text-sm font-medium text-secondary mb-2">换行处理</label>
<div class="space-y-2">
<label class="flex items-center space-x-2">
<input
v-model="options.removeEmptyLines"
type="checkbox"
class="form-checkbox"
>
<span class="text-secondary">移除空行</span>
</label>
<label class="flex items-center space-x-2">
<input
v-model="options.collapseEmptyLines"
type="checkbox"
class="form-checkbox"
>
<span class="text-secondary">合并连续空行</span>
</label>
<label class="flex items-center space-x-2">
<input
v-model="options.normalizeLineEndings"
type="checkbox"
class="form-checkbox"
>
<span class="text-secondary">统一换行符</span>
</label>
</div>
</div>
<!-- 其他选项 -->
<div>
<label class="block text-sm font-medium text-secondary mb-2">其他选项</label>
<div class="space-y-2">
<label class="flex items-center space-x-2">
<input
v-model="options.removeNonBreakingSpaces"
type="checkbox"
class="form-checkbox"
>
<span class="text-secondary">移除不间断空格 (&nbsp;)</span>
</label>
<label class="flex items-center space-x-2">
<input
v-model="options.removeZeroWidthSpaces"
type="checkbox"
class="form-checkbox"
>
<span class="text-secondary">移除零宽空格</span>
</label>
<label class="flex items-center space-x-2">
<input
v-model="options.trimLines"
type="checkbox"
class="form-checkbox"
>
<span class="text-secondary">修剪每行首尾空白</span>
</label>
</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-1 gap-2">
<button
v-for="preset in presets"
:key="preset.name"
@click="applyPreset(preset)"
class="text-left p-3 rounded bg-block hover:bg-block-hover text-secondary hover:text-primary transition-colors"
>
<div class="font-medium">{{ preset.name }}</div>
<div class="text-sm text-tertiary">{{ preset.description }}</div>
</button>
</div>
</div>
</div>
<!-- 输出区域 -->
<div class="space-y-4">
<!-- 输出文本 -->
<div class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">处理结果</h3>
<textarea
v-model="outputText"
readonly
placeholder="处理结果将显示在这里..."
class="textarea-field h-80 font-mono text-sm bg-block"
/>
<!-- 输出统计 -->
<div class="flex justify-between items-center mt-2 text-sm text-secondary">
<span>{{ outputStats.chars }} 字符 | {{ outputStats.lines }} </span>
<span>{{ outputStats.spaces }} 空格 | {{ outputStats.tabs }} 制表符</span>
</div>
</div>
<!-- 处理统计 -->
<div v-if="processStats" class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">处理统计</h3>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-secondary">字符变化:</span>
<span :class="processStats.charsDiff >= 0 ? 'text-error' : 'text-success'">
{{ processStats.charsDiff > 0 ? '+' : '' }}{{ processStats.charsDiff }}
</span>
</div>
<div class="flex justify-between">
<span class="text-secondary">行数变化:</span>
<span :class="processStats.linesDiff >= 0 ? 'text-error' : 'text-success'">
{{ processStats.linesDiff > 0 ? '+' : '' }}{{ processStats.linesDiff }}
</span>
</div>
<div class="flex justify-between">
<span class="text-secondary">空格移除:</span>
<span class="text-success">{{ processStats.spacesRemoved }}</span>
</div>
<div class="flex justify-between">
<span class="text-secondary">制表符移除:</span>
<span class="text-success">{{ processStats.tabsRemoved }}</span>
</div>
<div class="flex justify-between">
<span class="text-secondary">空行移除:</span>
<span class="text-success">{{ processStats.emptyLinesRemoved }}</span>
</div>
<div class="flex justify-between">
<span class="text-secondary">压缩率:</span>
<span class="text-primary">{{ processStats.compressionRatio }}%</span>
</div>
</div>
</div>
<!-- 字符分析 -->
<div v-if="charAnalysis" class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">字符分析</h3>
<div class="space-y-3">
<!-- 可见字符 -->
<div>
<div class="text-sm font-medium text-secondary mb-1">可见字符分布</div>
<div class="grid grid-cols-2 gap-2 text-xs">
<div class="flex justify-between">
<span class="text-tertiary">字母:</span>
<span class="text-primary">{{ charAnalysis.letters }}</span>
</div>
<div class="flex justify-between">
<span class="text-tertiary">数字:</span>
<span class="text-primary">{{ charAnalysis.numbers }}</span>
</div>
<div class="flex justify-between">
<span class="text-tertiary">标点:</span>
<span class="text-primary">{{ charAnalysis.punctuation }}</span>
</div>
<div class="flex justify-between">
<span class="text-tertiary">符号:</span>
<span class="text-primary">{{ charAnalysis.symbols }}</span>
</div>
</div>
</div>
<!-- 空白字符 -->
<div>
<div class="text-sm font-medium text-secondary mb-1">空白字符</div>
<div class="grid grid-cols-2 gap-2 text-xs">
<div class="flex justify-between">
<span class="text-tertiary">普通空格:</span>
<span class="text-primary">{{ charAnalysis.spaces }}</span>
</div>
<div class="flex justify-between">
<span class="text-tertiary">制表符:</span>
<span class="text-primary">{{ charAnalysis.tabs }}</span>
</div>
<div class="flex justify-between">
<span class="text-tertiary">换行符:</span>
<span class="text-primary">{{ charAnalysis.newlines }}</span>
</div>
<div class="flex justify-between">
<span class="text-tertiary">不间断空格:</span>
<span class="text-primary">{{ charAnalysis.nbspaces }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 处理日志 -->
<div v-if="processLog.length > 0" class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">处理日志</h3>
<div class="space-y-1 max-h-40 overflow-y-auto">
<div
v-for="(log, index) in processLog"
:key="index"
class="text-sm text-secondary"
>
{{ log }}
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useLanguage } from '@/composables/useLanguage'
const { t } = useLanguage()
// 响应式状态
const inputText = ref('')
const outputText = ref('')
const copied = ref(false)
const processLog = ref<string[]>([])
// 处理选项
const options = ref({
removeLeadingSpaces: false,
removeTrailingSpaces: false,
removeAllSpaces: false,
collapseSpaces: false,
removeTabs: false,
tabsToSpaces: false,
tabSize: 4,
removeEmptyLines: false,
collapseEmptyLines: false,
normalizeLineEndings: false,
removeNonBreakingSpaces: false,
removeZeroWidthSpaces: false,
trimLines: false
})
// 预设配置
const presets = [
{
name: '基本清理',
description: '移除行首尾空格,合并连续空格',
options: {
removeLeadingSpaces: false,
removeTrailingSpaces: true,
trimLines: true,
collapseSpaces: true,
collapseEmptyLines: true
}
},
{
name: '代码格式化',
description: '制表符转空格,统一缩进',
options: {
tabsToSpaces: true,
tabSize: 4,
normalizeLineEndings: true,
removeTrailingSpaces: true
}
},
{
name: '完全清理',
description: '移除所有不必要的空白字符',
options: {
removeAllSpaces: true,
removeTabs: true,
removeEmptyLines: true,
removeNonBreakingSpaces: true,
removeZeroWidthSpaces: true
}
},
{
name: '最小化',
description: '压缩到最小体积',
options: {
removeAllSpaces: true,
removeTabs: true,
removeEmptyLines: true,
removeNonBreakingSpaces: true,
removeZeroWidthSpaces: true,
normalizeLineEndings: true
}
}
]
// 计算统计信息
const inputStats = computed(() => {
return calculateStats(inputText.value)
})
const outputStats = computed(() => {
return calculateStats(outputText.value)
})
const processStats = computed(() => {
if (!outputText.value) return null
const input = inputStats.value
const output = outputStats.value
const charsDiff = output.chars - input.chars
const linesDiff = output.lines - input.lines
const spacesRemoved = input.spaces - output.spaces
const tabsRemoved = input.tabs - output.tabs
const emptyLinesRemoved = Math.max(0, input.emptyLines - output.emptyLines)
const compressionRatio = input.chars > 0
? Math.round((Math.abs(charsDiff) / input.chars) * 100)
: 0
return {
charsDiff,
linesDiff,
spacesRemoved,
tabsRemoved,
emptyLinesRemoved,
compressionRatio
}
})
const charAnalysis = computed(() => {
if (!inputText.value) return null
const text = inputText.value
let letters = 0, numbers = 0, punctuation = 0, symbols = 0
let spaces = 0, tabs = 0, newlines = 0, nbspaces = 0
for (const char of text) {
if (/[a-zA-Z\u4e00-\u9fff]/.test(char)) {
letters++
} else if (/\d/.test(char)) {
numbers++
} else if (/[.,;:!?]/.test(char)) {
punctuation++
} else if (/[^\s\w]/.test(char)) {
symbols++
} else if (char === ' ') {
spaces++
} else if (char === '\t') {
tabs++
} else if (char === '\n' || char === '\r') {
newlines++
} else if (char === '\u00A0') {
nbspaces++
}
}
return {
letters,
numbers,
punctuation,
symbols,
spaces,
tabs,
newlines,
nbspaces
}
})
// 计算文本统计
const calculateStats = (text: string) => {
const chars = text.length
const lines = text ? text.split('\n').length : 0
const spaces = (text.match(/ /g) || []).length
const tabs = (text.match(/\t/g) || []).length
const emptyLines = text ? text.split('\n').filter(line => line.trim() === '').length : 0
return { chars, lines, spaces, tabs, emptyLines }
}
// 处理文本
const processText = () => {
if (!inputText.value.trim()) return
let result = inputText.value
processLog.value = []
// 移除零宽空格
if (options.value.removeZeroWidthSpaces) {
const before = result.length
result = result.replace(/[\u200B-\u200D\uFEFF]/g, '')
const removed = before - result.length
if (removed > 0) {
processLog.value.push(`移除了 ${removed} 个零宽空格`)
}
}
// 移除不间断空格
if (options.value.removeNonBreakingSpaces) {
const before = (result.match(/\u00A0/g) || []).length
result = result.replace(/\u00A0/g, ' ')
if (before > 0) {
processLog.value.push(`转换了 ${before} 个不间断空格`)
}
}
// 制表符转空格
if (options.value.tabsToSpaces) {
const before = (result.match(/\t/g) || []).length
const spaces = ' '.repeat(options.value.tabSize)
result = result.replace(/\t/g, spaces)
if (before > 0) {
processLog.value.push(`转换了 ${before} 个制表符为空格`)
}
}
// 移除制表符
if (options.value.removeTabs && !options.value.tabsToSpaces) {
const before = (result.match(/\t/g) || []).length
result = result.replace(/\t/g, '')
if (before > 0) {
processLog.value.push(`移除了 ${before} 个制表符`)
}
}
// 统一换行符
if (options.value.normalizeLineEndings) {
result = result.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
processLog.value.push('统一了换行符为 LF')
}
// 按行处理
let lines = result.split('\n')
// 移除行首空格
if (options.value.removeLeadingSpaces) {
const before = lines.join('').length
lines = lines.map(line => line.replace(/^[ \t]+/, ''))
const removed = before - lines.join('').length
if (removed > 0) {
processLog.value.push(`移除了 ${removed} 个行首空白字符`)
}
}
// 移除行尾空格
if (options.value.removeTrailingSpaces) {
const before = lines.join('').length
lines = lines.map(line => line.replace(/[ \t]+$/, ''))
const removed = before - lines.join('').length
if (removed > 0) {
processLog.value.push(`移除了 ${removed} 个行尾空白字符`)
}
}
// 修剪每行首尾空白
if (options.value.trimLines) {
const before = lines.join('').length
lines = lines.map(line => line.trim())
const removed = before - lines.join('').length
if (removed > 0) {
processLog.value.push(`修剪了行首尾空白,移除 ${removed} 个字符`)
}
}
// 移除空行
if (options.value.removeEmptyLines) {
const before = lines.length
lines = lines.filter(line => line.trim() !== '')
const removed = before - lines.length
if (removed > 0) {
processLog.value.push(`移除了 ${removed} 个空行`)
}
}
// 合并连续空行
if (options.value.collapseEmptyLines && !options.value.removeEmptyLines) {
const before = lines.length
const collapsed: string[] = []
let lastWasEmpty = false
for (const line of lines) {
const isEmpty = line.trim() === ''
if (!isEmpty || !lastWasEmpty) {
collapsed.push(line)
}
lastWasEmpty = isEmpty
}
lines = collapsed
const removed = before - lines.length
if (removed > 0) {
processLog.value.push(`合并了连续空行,移除 ${removed}`)
}
}
result = lines.join('\n')
// 移除所有空格
if (options.value.removeAllSpaces) {
const before = (result.match(/ /g) || []).length
result = result.replace(/ /g, '')
if (before > 0) {
processLog.value.push(`移除了所有 ${before} 个空格`)
}
}
// 合并连续空格
if (options.value.collapseSpaces && !options.value.removeAllSpaces) {
const before = result.length
result = result.replace(/ +/g, ' ')
const removed = before - result.length
if (removed > 0) {
processLog.value.push(`合并连续空格,移除 ${removed} 个字符`)
}
}
outputText.value = result
if (processLog.value.length === 0) {
processLog.value.push('没有需要处理的内容')
}
}
// 应用预设
const applyPreset = (preset: any) => {
// 重置所有选项
Object.keys(options.value).forEach(key => {
options.value[key as keyof typeof options.value] = false
})
// 应用预设选项
Object.assign(options.value, preset.options)
// 如果有输入文本,立即处理
if (inputText.value.trim()) {
processText()
}
}
// 复制结果
const copyResult = 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)
}
}
// 交换内容
const swapContent = () => {
const temp = inputText.value
inputText.value = outputText.value
outputText.value = temp
}
// 清除所有
const clearAll = () => {
inputText.value = ''
outputText.value = ''
processLog.value = []
}
// 更新统计
const updateStats = () => {
// 自动更新统计信息
}
// 监听输入变化,自动处理
watch(() => options.value, () => {
if (inputText.value.trim()) {
processText()
}
}, { deep: true })
</script>