Skip to content

阅读时间统计

自动计算并显示文章的预估阅读时间。

组件实现

vue
<!-- .vitepress/theme/components/ReadingTime.vue -->
<script setup lang="ts">
import { useData } from 'vitepress'
import { computed } from 'vue'

const { page } = useData()

interface Props {
  wordsPerMinute?: number
  template?: string
}

const props = withDefaults(defineProps<Props>(), {
  wordsPerMinute: 300,  // 中文约 300 字/分钟
  template: '{min} 分钟阅读'
})

const readingTime = computed(() => {
  const content = page.value.content
  if (!content) return 0
  
  // 计算字数(中文)
  const chineseChars = (content.match(/[\u4e00-\u9fa5]/g) || []).length
  // 计算英文单词
  const englishWords = (content.match(/[a-zA-Z]+/g) || []).length
  
  // 总字数(英文单词计为 1 字)
  const totalWords = chineseChars + englishWords
  
  // 计算分钟数
  const minutes = Math.ceil(totalWords / props.wordsPerMinute)
  
  return Math.max(1, minutes)
})

const displayText = computed(() => {
  return props.template.replace('{min}', String(readingTime.value))
})
</script>

<template>
  <span class="reading-time">
    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
      <circle cx="12" cy="12" r="10"></circle>
      <polyline points="12 6 12 12 16 14"></polyline>
    </svg>
    <span>{{ displayText }}</span>
  </span>
</template>

<style scoped>
.reading-time {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  color: var(--vp-c-text-2);
  font-size: 0.85rem;
}

.reading-time svg {
  opacity: 0.7;
}
</style>

注册全局组件

ts
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import ReadingTime from './components/ReadingTime.vue'

export default {
  extends: DefaultTheme,
  enhanceApp({ app }) {
    app.component('ReadingTime', ReadingTime)
  }
}

在文档中使用

方式一:在模板中使用

ts
// .vitepress/theme/index.ts
import { h } from 'vue'
import DefaultTheme from 'vitepress/theme'
import ReadingTime from './components/ReadingTime.vue'

export default {
  extends: DefaultTheme,
  Layout: () => {
    return h(DefaultTheme.Layout, null, {
      'doc-before': () => h(ReadingTime)
    })
  }
}

方式二:在 Markdown 中使用

markdown
---
title: 我的文章
---

<ReadingTime />

## 文章内容

...

高级:带字数统计

vue
<script setup lang="ts">
import { useData } from 'vitepress'
import { computed } from 'vue'

const { page } = useData()

const stats = computed(() => {
  const content = page.value.content || ''
  
  // 中文字符
  const chineseChars = (content.match(/[\u4e00-\u9fa5]/g) || []).length
  // 英文单词
  const englishWords = (content.match(/[a-zA-Z]+/g) || []).length
  // 代码块行数
  const codeLines = (content.match(/```[\s\S]*?```/g) || [])
    .reduce((acc, block) => acc + block.split('\n').length - 2, 0)
  
  // 总字数
  const totalWords = chineseChars + englishWords
  // 阅读时间(分钟)
  const readingMinutes = Math.max(1, Math.ceil(totalWords / 300))
  
  return {
    words: totalWords,
    chinese: chineseChars,
    english: englishWords,
    codeLines,
    readingMinutes
  }
})
</script>

<template>
  <div class="article-stats">
    <span class="stat">
      <svg width="16" height="16" viewBox="0 0 24 24">
        <path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
      </svg>
      {{ stats.words }} 字
    </span>
    <span class="stat">
      <svg width="16" height="16" viewBox="0 0 24 24">
        <circle cx="12" cy="12" r="10"></circle>
        <polyline points="12 6 12 12 16 14"></polyline>
      </svg>
      {{ stats.readingMinutes }} 分钟
    </span>
    <span class="stat" v-if="stats.codeLines > 0">
      <svg width="16" height="16" viewBox="0 0 24 24">
        <polyline points="16 18 22 12 16 6"></polyline>
        <polyline points="8 6 2 12 8 18"></polyline>
      </svg>
      {{ stats.codeLines }} 行代码
    </span>
  </div>
</template>

<style scoped>
.article-stats {
  display: flex;
  gap: 1rem;
  padding: 0.75rem 0;
  margin-bottom: 1rem;
  border-bottom: 1px solid var(--vp-c-divider);
}

.stat {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  color: var(--vp-c-text-2);
  font-size: 0.85rem;
}

.stat svg {
  stroke: currentColor;
  stroke-width: 2;
  fill: none;
}
</style>

在列表页显示

vue
<!-- 在博客列表中使用 -->
<script setup lang="ts">
import ReadingTime from './ReadingTime.vue'

const posts = [
  { title: '文章 1', content: '...' },
  { title: '文章 2', content: '...' }
]

function getReadingTime(content: string) {
  const words = (content.match(/[\u4e00-\u9fa5]|[a-zA-Z]+/g) || []).length
  return Math.max(1, Math.ceil(words / 300))
}
</script>

<template>
  <article v-for="post in posts" :key="post.title">
    <h2>{{ post.title }}</h2>
    <span class="time">{{ getReadingTime(post.content) }} 分钟阅读</span>
  </article>
</template>

参考链接

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献