Files
Thing/src/modules/devtools/tools/CharCountTools.vue
T
2026-09-12 11:05:26 +08:00

130 lines
4.7 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.
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
const input = ref('')
const showFrequency = ref(true)
function utf8Bytes(s: string): number {
return new TextEncoder().encode(s).length
}
/** 统计词:中文按字、英文按单词 */
function countWords(s: string): number {
const cjk = (s.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) ?? []).length
const latin = (s.match(/[a-zA-Z0-9]+(?:[-'][a-zA-Z0-9]+)*/g) ?? []).length
return cjk + latin
}
interface Stats {
chars: number
charsNoSpace: number
bytes: number
words: number
lines: number
nonEmptyLines: number
sentences: number
paragraphs: number
}
const stats = computed<Stats>(() => {
const v = input.value
const lines = v === '' ? 0 : v.split('\n').length
const nonEmpty = v.split('\n').filter(l => l.trim()).length
// 句子:以 。!?.!?;; 结尾的段落片段
const sentences = (v.match(/[^。!?!?\n]+[。!?!?]?/g) ?? []).filter(s => s.trim()).length
const paragraphs = v.split(/\n\s*\n/).filter(p => p.trim()).length
return {
chars: v.length,
charsNoSpace: v.replace(/\s/g, '').length,
bytes: utf8Bytes(v),
words: countWords(v),
lines,
nonEmptyLines: nonEmpty,
sentences,
paragraphs
}
})
const FIELDS: Array<{ key: keyof Stats; label: string }> = [
{ key: 'chars', label: '字符数' },
{ key: 'charsNoSpace', label: '字符数(不含空白)' },
{ key: 'bytes', label: '字节数(UTF-8' },
{ key: 'words', label: '词数(中文按字/英文按词)' },
{ key: 'lines', label: '行数' },
{ key: 'nonEmptyLines', label: '非空行数' },
{ key: 'sentences', label: '句子数' },
{ key: 'paragraphs', label: '段落数(空行分隔)' }
]
/** 高频字符 / 高频词 top 10 */
const topChars = computed(() => {
if (!showFrequency.value || !input.value) return []
const map = new Map<string, number>()
for (const ch of input.value) {
if (/\s/.test(ch)) continue
map.set(ch, (map.get(ch) ?? 0) + 1)
}
return [...map.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10)
})
const topWords = computed(() => {
if (!showFrequency.value || !input.value) return []
const tokens = input.value.match(/[\u4e00-\u9fff]|[a-zA-Z0-9]+(?:[-'][a-zA-Z0-9]+)*/g) ?? []
const map = new Map<string, number>()
for (const t of tokens) {
map.set(t, (map.get(t) ?? 0) + 1)
}
return [...map.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10)
})
</script>
<template>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1.5">
<div class="flex items-center justify-between">
<Label class="text-xs">文本</Label>
<label class="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
显示高频统计
<Switch v-model="showFrequency" />
</label>
</div>
<Textarea v-model="input" placeholder="粘贴或输入文本,实时统计..." class="min-h-[160px] font-mono text-xs resize-y" />
</div>
<div class="rounded-md border border-border divide-y divide-border text-xs">
<div v-for="f in FIELDS" :key="f.key" class="flex justify-between px-3 py-1.5">
<span class="text-muted-foreground">{{ f.label }}</span>
<span class="font-mono">{{ stats[f.key].toLocaleString() }}</span>
</div>
</div>
<template v-if="showFrequency && input">
<div class="grid grid-cols-2 gap-3">
<div class="flex flex-col gap-1.5">
<Label class="text-xs">高频字符 Top 10</Label>
<div class="rounded-md border border-border divide-y divide-border text-xs">
<div v-for="([ch, n], i) in topChars" :key="i" class="flex justify-between px-3 py-1">
<span class="font-mono w-8 text-center rounded bg-muted">{{ ch }}</span>
<span class="font-mono text-muted-foreground">{{ n }} </span>
</div>
<div v-if="topChars.length === 0" class="px-3 py-2 text-muted-foreground">无内容</div>
</div>
</div>
<div class="flex flex-col gap-1.5">
<Label class="text-xs">高频词 Top 10</Label>
<div class="rounded-md border border-border divide-y divide-border text-xs">
<div v-for="([w, n], i) in topWords" :key="i" class="flex justify-between px-3 py-1">
<span class="font-mono">{{ w }}</span>
<span class="font-mono text-muted-foreground">{{ n }} </span>
</div>
<div v-if="topWords.length === 0" class="px-3 py-2 text-muted-foreground">无内容</div>
</div>
</div>
</div>
</template>
</div>
</template>