文章字数统计
实现文章字数统计,包括中文、英文和代码统计。
组件实现
vue
<!-- .vitepress/theme/components/WordCount.vue -->
<script setup lang="ts">
import { useData } from 'vitepress'
import { computed } from 'vue'
const { page } = useData()
interface Props {
showDetails?: boolean
}
const props = withDefaults(defineProps<Props>(), {
showDetails: false
})
const count = computed(() => {
const content = page.value.content || ''
// 中文字符数
const chinese = (content.match(/[\u4e00-\u9fa5]/g) || []).length
// 英文单词数
const english = (content.match(/[a-zA-Z]+/g) || []).length
// 数字
const numbers = (content.match(/\d+/g) || []).length
// 标点符号
const punctuation = (content.match(/[,。!?;:""''、()【】]/g) || []).length
// 总字数(中文 + 英文单词 + 数字)
const total = chinese + english + numbers
// 代码块字数
const codeBlocks = content.match(/```[\s\S]*?```/g) || []
const codeChars = codeBlocks.reduce((acc, block) =>
acc + block.replace(/```\w*\n?/g, '').length, 0
)
return {
chinese,
english,
numbers,
punctuation,
total,
codeChars,
codeBlocks: codeBlocks.length
}
})
</script>
<template>
<div class="word-count">
<span class="count-item total">
<span class="label">总字数</span>
<span class="value">{{ count.total }}</span>
</span>
<template v-if="showDetails">
<span class="count-item">
<span class="label">中文</span>
<span class="value">{{ count.chinese }}</span>
</span>
<span class="count-item">
<span class="label">英文</span>
<span class="value">{{ count.english }}</span>
</span>
<span class="count-item" v-if="count.codeBlocks > 0">
<span class="label">代码块</span>
<span class="value">{{ count.codeBlocks }}</span>
</span>
</template>
</div>
</template>
<style scoped>
.word-count {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
padding: 0.5rem 0;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.count-item {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.count-item.total {
font-weight: 500;
color: var(--vp-c-text-1);
}
.label {
opacity: 0.7;
}
.value {
font-variant-numeric: tabular-nums;
}
</style>统计工具函数
ts
// .vitepress/theme/utils/wordCount.ts
export interface WordStats {
chinese: number // 中文字符
english: number // 英文单词
numbers: number // 数字
total: number // 总字数
codeChars: number // 代码字符
codeBlocks: number // 代码块数量
images: number // 图片数量
links: number // 链接数量
paragraphs: number // 段落数量
}
export function countWords(content: string): WordStats {
// 移除 YAML frontmatter
const body = content.replace(/^---[\s\S]*?---\n?/, '')
// 移除 HTML 注释
const clean = body.replace(/<!--[\s\S]*?-->/g, '')
// 中文字符
const chinese = (clean.match(/[\u4e00-\u9fa5]/g) || []).length
// 英文单词
const english = (clean.match(/[a-zA-Z]+/g) || []).length
// 数字
const numbers = (clean.match(/\d+/g) || []).length
// 代码块
const codeBlocks = clean.match(/```[\s\S]*?```/g) || []
const codeChars = codeBlocks.reduce((acc, block) => {
const code = block.replace(/```\w*\n?/g, '').replace(/\n/g, '')
return acc + code.length
}, 0)
// 图片
const images = (clean.match(/!\[.*?\]\(.*?\)/g) || []).length
// 链接(排除图片)
const links = (clean.match(/(?<!\!)\[.*?\]\(.*?\)/g) || []).length
// 段落(非空行)
const paragraphs = clean.split('\n').filter(line =>
line.trim() && !line.match(/^[#*>\-`]/)
).length
return {
chinese,
english,
numbers,
total: chinese + english + numbers,
codeChars,
codeBlocks: codeBlocks.length,
images,
links,
paragraphs
}
}
// 格式化显示
export function formatWordCount(count: number): string {
if (count < 1000) return `${count} 字`
if (count < 10000) return `${(count / 1000).toFixed(1)}k 字`
return `${(count / 10000).toFixed(1)}w 字`
}在构建时生成统计
ts
// scripts/stats.ts
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
interface ArticleStats {
path: string
title: string
words: number
readingTime: number
date: string
}
const stats: ArticleStats[] = []
function walk(dir: string) {
const files = fs.readdirSync(dir)
for (const file of files) {
const filePath = path.join(dir, file)
const stat = fs.statSync(filePath)
if (stat.isDirectory()) {
walk(filePath)
} else if (file.endsWith('.md')) {
const content = fs.readFileSync(filePath, 'utf-8')
const { data, content: body } = matter(content)
const chinese = (body.match(/[\u4e00-\u9fa5]/g) || []).length
const english = (body.match(/[a-zA-Z]+/g) || []).length
const words = chinese + english
stats.push({
path: filePath.replace('docs', '').replace('.md', ''),
title: data.title || file,
words,
readingTime: Math.max(1, Math.ceil(words / 300)),
date: data.date || ''
})
}
}
}
walk('docs')
// 按字数排序
stats.sort((a, b) => b.words - a.words)
// 输出统计
console.log('=== 文章统计 ===')
console.log(`总文章数: ${stats.length}`)
console.log(`总字数: ${stats.reduce((acc, s) => acc + s.words, 0).toLocaleString()}`)
console.log(`平均字数: ${Math.round(stats.reduce((acc, s) => acc + s.words, 0) / stats.length)}`)
// 保存到 JSON
fs.writeFileSync('docs/public/article-stats.json', JSON.stringify(stats, null, 2))显示站点总字数
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
interface ArticleStats {
path: string
words: number
}
const stats = ref<ArticleStats[]>([])
const totalWords = ref(0)
onMounted(async () => {
const res = await fetch('/article-stats.json')
stats.value = await res.json()
totalWords.value = stats.value.reduce((acc, s) => acc + s.words, 0)
})
</script>
<template>
<div class="site-stats">
<div class="stat">
<span class="number">{{ stats.length }}</span>
<span class="label">篇文章</span>
</div>
<div class="stat">
<span class="number">{{ totalWords.toLocaleString() }}</span>
<span class="label">字</span>
</div>
</div>
</template>
<style scoped>
.site-stats {
display: flex;
gap: 2rem;
}
.stat {
text-align: center;
}
.number {
display: block;
font-size: 2rem;
font-weight: 700;
color: var(--vp-c-brand-1);
}
.label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
</style>