130 lines
4.7 KiB
Vue
130 lines
4.7 KiB
Vue
<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>
|