Skip to content

文章字数统计

实现文章字数统计,包括中文、英文和代码统计。

组件实现

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>

参考链接

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献