Skip to content

功能魔改

文章统计信息显示

效果图如下:

img

.vitepress\theme\untils\tools.ts文件中写入以下内容,包含了字数统计、阅读时间计算(含图片估算)、数字格式化和日期格式化。

js
// .vitepress/utils/tools.ts

/**
 * 阅读时间计算(包含图片估算)
 */
export const calculateReadTime = (wordCount: number, imageCount: number): number => {
    // 假设阅读速度:中文 275字/分钟
    const wordTime = (wordCount / 275) * 60 // 秒

    // 图片阅读时间估算
    let imageTime = 0
    const n = imageCount
    if (n > 0) {
        if (n <= 10) {
            // 等差数列求和:13 + 14 + 15 ...
            imageTime = n * 13 + (n * (n - 1)) / 2
        } else {
            // 超过10张,每张按3秒估算
            imageTime = 175 + (n - 10) * 3
        }
    }

    // 总分钟数,向上取整
    return Math.ceil((wordTime + imageTime) / 60)
}

/**
 * 文字统计 (中英文混合)
 */
export const countWord = (data: string): number => {
    if (!data) return 0
    // 简单清洗,去除Markdown链接语法等,保留文本
    const cleanData = data.replace(/!\[.*?\]\(.*?\)|\[.*?\]\(.*?\)|<.*?>/g, '')

    const cjkPattern = /[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF\u3040-\u309F\uAC00-\uD7AF]/g
    const wordPattern = /[a-zA-Z0-9_\u00C0-\u00FF]+/g

    const cjkMatches = cleanData.match(cjkPattern) || []
    const wordMatches = cleanData.match(wordPattern) || []

    return cjkMatches.length + wordMatches.length
}

/**
 * 数字千分位转换 (e.g. 1500 -> 1.5k)
 */
export const countTransK = (count: number): string => {
    return new Intl.NumberFormat('en-US', {
        notation: 'compact',
        maximumFractionDigits: 1
    }).format(count)
}

/**
 * 日期格式化
 */
export const formatDate = (hasTime = false): Intl.DateTimeFormat => {
    const options: Intl.DateTimeFormatOptions = {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
        ...(hasTime && {
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit',
            hour12: false
        })
    }
    return new Intl.DateTimeFormat('zh-CN', options)
}

.vitepress\theme\components\ArticleInfo.vue文件中写入以下内容,创建元数据组件,会自动抓取页面文字、监听 PV 数据并显示。

vue
<!-- .vitepress/theme/components/ArticleInfo.vue -->
<script setup lang="ts">
import { useData } from 'vitepress'
import { ref, onMounted, computed, onUnmounted, nextTick, watch } from 'vue'
import { countWord, formatDate, calculateReadTime, countTransK } from '../../theme/untils/tools'

const formatWordCount = (count: number): string => {
    // 如果字数大于等于 1000
    if (count >= 1000) {
        // 除以 1000 并保留 1 位小数,加上 'k'
        return (count / 1000).toFixed(1) + 'k'
    }
    // 小于 1000 直接返回原数字
    return count.toString()
}

const { frontmatter, page } = useData()

// --- 日期处理 ---
const dateFormatter = formatDate()
const format = (date: string | number | Date | undefined) => {
    if (!date) return ''
    return dateFormatter.format(new Date(date)).replace(/\//g, '-')
}

// 优先显示 frontmatter.date 作为发布时间,否则为空
const firstCommit = computed(() => format(frontmatter.value.date || frontmatter.value.firstCommit))
const lastUpdated = computed(() => format(frontmatter.value.lastUpdated || page.value.lastUpdated))

// --- 字数与阅读时间统计 ---
const wordCount = ref(0)
const readTime = ref(0)

const updateStats = () => {
    const docDomContainer = document.querySelector('#VPContent')
    if (!docDomContainer) return

    // 统计图片
    const imgs = docDomContainer.querySelectorAll('.content-container .main img')
    const imageCount = imgs.length

    // 统计文字
    const content = docDomContainer.querySelector('.content-container .main')?.textContent || ''
    const words = countWord(content)
    wordCount.value = words

    // 计算阅读时间
    readTime.value = calculateReadTime(words, imageCount)
}

// --- PV 统计 (监听 Busuanzi 或其他脚本写入) ---
const pv = ref('∞') // 初始状态
let observer: MutationObserver | null = null

const initPVObserver = () => {
    // 1. 寻找 Busuanzi 生成的标准 ID
    let pvEl = document.getElementById('busuanzi_value_page_pv')

    // 2. 如果没找到,尝试寻找我们自定义的隐藏 span (兼容性处理)
    if (!pvEl) {
        pvEl = document.getElementById('vercount_value_page_pv')
    }

    if (!pvEl) {
        pv.value = '-'
        return
    }

    const readPv = () => {
        const text = pvEl.textContent?.trim()
        if (text) {
            const val = parseInt(text)
            if (!isNaN(val)) {
                pv.value = countTransK(val)
                if (observer) observer.disconnect() // 获取成功后停止监听
            }
        }
    }

    if (pvEl.textContent) readPv()
    else {
        observer = new MutationObserver(readPv)
        observer.observe(pvEl, { childList: true, characterData: true, subtree: true })
    }
}

// --- 生命周期 ---
onMounted(() => {
    nextTick(() => {
        updateStats()
        initPVObserver()
    })
})

onUnmounted(() => {
    observer?.disconnect()
})

watch(
    () => page.value.relativePath,
    () => {
        pv.value = '...'
        nextTick(() => {
            updateStats()
            if (observer) observer.disconnect()
            initPVObserver()
        })
    }
)
</script>

<template>
    <div class="article-info">
        <!-- 必须保留这个隐藏的 span,供 Busuanzi 脚本写入数据 -->
        <span id="busuanzi_container_page_pv"
            style="position: absolute; width: 0; height: 0; overflow: hidden; opacity: 0; pointer-events: none;">
            <span id="busuanzi_value_page_pv"></span>
        </span>

        <div class="info-item" v-if="firstCommit" title="发布时间">
            <svg fill="none" viewBox="0 0 24 24" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
                <path
                    d="m17 3h4c.2652 0 .5196.10536.7071.29289.1875.18754.2929.44189.2929.70711v16c0 .2652-.1054.5196-.2929.7071s-.4419.2929-.7071.2929h-18c-.26522 0-.51957-.1054-.70711-.2929-.18753-.1875-.29289-.4419-.29289-.7071v-16c0-.26522.10536-.51957.29289-.70711.18754-.18753.44189-.29289.70711-.29289h4v-2h2v2h6v-2h2zm-13 6v10h16v-10zm2 2h2v2h-2zm0 4h2v2h-2zm4-4h8v2h-8zm0 4h5v2h-5z"
                    fill="#8a8a8a" />
            </svg>
            <span>发布于: {{ firstCommit }}</span>
        </div>

        <div class="info-item" v-if="lastUpdated" title="更新时间">
            <svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="16" height="16" viewBox="0 0 48 48">
                <path
                    d="M24,4C13,4,4,13,4,24s9,20,20,20s20-9,20-20S35,4,24,4z M31.5,27h-8c-0.8,0-1.5-0.7-1.5-1.5v-12c0-0.8,0.7-1.5,1.5-1.5	s1.5,0.7,1.5,1.5V24h6.5c0.8,0,1.5,0.7,1.5,1.5S32.3,27,31.5,27z"
                    fill="#8a8a8a">
                </path>
            </svg>
            <span>更新于: {{ lastUpdated }}</span>
        </div>

        <div class="info-item" title="字数统计">
            <svg class="icon" viewBox="0 0 1024 1024" width="16" height="16">
                <path
                    d="M204.8 0h477.866667l273.066666 273.066667v614.4c0 75.093333-61.44 136.533333-136.533333 136.533333H204.8c-75.093333 0-136.533333-61.44-136.533333-136.533333V136.533333C68.266667 61.44 129.706667 0 204.8 0z m307.2 607.573333l68.266667 191.146667c13.653333 27.306667 54.613333 27.306667 61.44 0l102.4-273.066667c6.826667-20.48 0-34.133333-20.48-40.96s-34.133333 0-40.96 13.653334l-68.266667 191.146666-68.266667-191.146666c-13.653333-27.306667-54.613333-27.306667-68.266666 0l-68.266667 191.146666-68.266667-191.146666c-6.826667-13.653333-27.306667-27.306667-47.786666-20.48s-27.306667 27.306667-20.48 47.786666l102.4 273.066667c13.653333 27.306667 54.613333 27.306667 61.44 0l75.093333-191.146667z"
                    fill="#777777"></path>
            </svg>
            <span>字数: {{ formatWordCount(wordCount) }}</span>
        </div>

        <div class="info-item" title="阅读时间">
            <svg class="icon" viewBox="0 0 1060 1024" width="16" height="16">
                <path
                    d="M556.726857 0.256A493.933714 493.933714 0 0 0 121.929143 258.998857L0 135.021714v350.390857h344.649143L196.205714 334.482286a406.820571 406.820571 0 1 1-15.908571 312.649143H68.937143A505.819429 505.819429 0 1 0 556.726857 0.256z m-79.542857 269.531429v274.907428l249.197714 150.966857 42.422857-70.070857-212.114285-129.389714V269.787429h-79.542857z"
                    fill="#8a8a8a"></path>
            </svg>
            <span>时长: {{ readTime }} 分钟</span>
        </div>

        <div class="info-item" title="阅读量">
            <svg class="icon" viewBox="0 0 1024 1024" width="16" height="16">
                <path
                    d="M512 512c-114.688 0-209.92-95.232-209.92-209.92S397.312 92.16 512 92.16s209.92 95.232 209.92 209.92S626.688 512 512 512z"
                    fill="#8a8a8a"></path>
                <path
                    d="M906.24 931.84c-20.48-266.24-245.76-389.12-394.24-389.12S137.216 665.6 117.76 931.84c0 20.48 15.36 35.84 35.84 35.84h716.8c20.48 0 35.84-15.36 35.84-35.84z"
                    fill="#8a8a8a"></path>
            </svg>
            <span>阅读量: {{ pv }}</span>
        </div>
    </div>
</template>

<style scoped>
.article-info {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    margin-top: 1rem;
    margin-bottom: 1.5rem;
    color: var(--vp-c-text-2);
    font-size: 0.9rem;
}

.info-item {
    display: flex;
    align-items: center;
    gap: 0.25rem;
}

.icon {
    display: inline-block;
    width: 1em;
    height: 1em;
    fill: currentColor;
    opacity: 0.8;
}
</style>

.vitepress/theme/index.ts中,将组件注册为全局组件。

js
import ArticleInfo from './components/ArticleInfo.vue'

export default {
  extends: DefaultTheme,
  enhanceApp({app}) { 
    // 注册全局组件
    app.component('ArticleInfo', ArticleInfo);  
  }
}

.vitepress\config.mjs文件中写入以下内容。

js
import { defineConfig } from 'vitepress'

export default defineConfig({

  markdown: {	
    // 组件插入h1标题下
    config: (md) => {	
      // 使用 markdown-it 插件
      md.use((md) => {	
        // 渲染规则:在 H1 标签结束后插入组件
        const originalHeadingClose = md.renderer.rules.heading_close || function (tokens, idx, options, env, self) {	
          return self.renderToken(tokens, idx, options);	
        };	

        md.renderer.rules.heading_close = (tokens, idx, options, env, slf) => {	
          const htmlResult = originalHeadingClose(tokens, idx, options, env, slf);	

          // 只有当标题是 h1 时才插入
          if (tokens[idx].tag === 'h1') {	
            return htmlResult + `<ArticleInfo />`;	
          }	

          return htmlResult;	
        };	
      })	
    }	
  }	
    
})

隐藏默认在文章底部显示的更新时间,在.vitepress\theme\style\var.css中加入以下内容。

css
.VPLastUpdated {
    display: none;
}

@media (min-width: 640px) {
    .VPLastUpdated {
        display: none;
    }
}

图片居中

.vitepress\theme\style\var.css 文件中写入以下内容。

css
/************************* 设置图片居中 *************************/
p img {
    display: block;
    margin: 0 auto;
}
/************************* 设置图片居中 *************************/

创建 .vitepress\theme\style\index.css 文件,并引入第一步创建的css文件

css
@import './var.css';

.vitepress/theme/index.js 文件中引入index.css

js
import './style/index.css'

参考链接:

代码框Mac风格样式

.vitepress\theme\style\index.css 文件中引入新创建的css文件

css
@import './blur.css';

.vitepress\theme\style\blur.css 文件中写入以下内容。

css
/************************ 代码块 ************************/

/* 代码块:增加留空边距 增加阴影 */
.vp-doc div[class*=language-] {
    box-shadow: 0 10px 30px 0 var(--vp-c-gray-soft);
    border: solid 1px var(--vp-c-gray-1);
    padding-top: 20px;
    margin: auto;
    border-radius: 8px;
}

/* 代码块:添加macOS风格的小圆点 */
.vp-doc div[class*=language-]::before {
  content: "";
  display: block;
  position: absolute;
  top: 12px;
  left: 12px;
  width: 12px;
  height: 12px;
  background-color: #ff5f56;
  border-radius: 50%;
  box-shadow: 20px 0 0 #ffbd2e, 40px 0 0 #27c93f;
  z-index: 1;
}

/* 代码块:下移行号 隐藏右侧竖线 */
.vp-doc .line-numbers-wrapper {
  padding-top: 40px;
  border-right: none;
}

/* 代码块:重建行号右侧竖线 */
.vp-doc .line-numbers-wrapper::after {
  content: "";
  position: absolute;
  top: 40px;
  right: 0;
  border-right: 1px solid var(--vp-code-block-divider-color);
  height: calc(100% - 60px);
}

.vp-doc div[class*='language-'].line-numbers-mode {
  margin-bottom: 20px;
}
/************************ 代码块 ************************/


/************************ 代码组 ************************/

/* 代码组:tab间距 */
.vp-code-group .tabs {
  padding-top: 20px;
  position: relative; /* 为绝对定位的小圆点提供参考 */
  margin: auto;
  border-radius: 8px 8px 0 0;
}

/* 代码组:添加样式及阴影 */
.vp-code-group {
  color: var(--vp-c-black-soft);
  border-radius: 8px;
  box-shadow: 0 10px 30px 0 var(--vp-c-gray-soft);
  border: solid 1px var(--vp-c-gray-1);
  background: #f6f6f7;
}

/* 应用夜间模式样式 */
.dark .vp-code-group {
  background: #161618;
}

/* 代码组:添加macOS风格的小圆点 */
.vp-code-group .tabs::before {
  content: ' ';
  position: absolute;
  top: 12px;
  left: 12px;
  height: 12px;
  width: 12px;
  background: #fc625d;
  border-radius: 50%;
  box-shadow: 20px 0 #fdbc40, 40px 0 #35cd4b;
}

/* 代码组:修正倒角、阴影、边距 */
.vp-code-group div[class*="language-"] {
  border-radius: 8px;
  box-shadow: none;
  padding-top: 0px;
  border: solid 1px var(--vp-custom-block-note-border);
}

/* 代码组:隐藏小圆点 */
.vp-code-group div[class*="language-"]::before {
  display: none;
}

/* 代码组:修正行号位置 */
.vp-code-group .line-numbers-mode .line-numbers-wrapper {
  padding-top: 20px;
}

/* 代码组:修正行号右侧竖线位置 */
.vp-code-group .line-numbers-mode .line-numbers-wrapper::after {
  top: 24px;
  height: calc(100% - 45px);
}

/* 代码组(无行号):修正倒角、阴影、边距 */
.vp-code-group div[class*="language-"].vp-adaptive-theme {
  border-radius: 8px;
  box-shadow: none;
  padding-top: 0px;
}

/* 代码组(无行号):隐藏小圆点 */
.vp-code-group div[class*="language-"].vp-adaptive-theme::before {
  display: none;
}
/************************ 代码组 ************************/

代码组图标

shell
npm install vitepress-plugin-group-icons  --force

.vitepress\config.mjs写入以下内容。

js
import { groupIconMdPlugin, groupIconVitePlugin } from 'vitepress-plugin-group-icons'

export default defineConfig({
  markdown: {      
    config(md) {    
      md.use(groupIconMdPlugin)    
    },   
  },     
  vite: {     
    plugins: [    
      groupIconVitePlugin()    
    ],    
  }    
})

在.vitepress\theme\index.js中写入以下内容。

shell
import 'virtual:group-icons.css'

重新启动后查看运行情况如下。

img

链接卡片

.vitepress\theme\components\Linkcard.vue文件中写入以下内容。

vue
<script setup lang="ts">
interface Props {
    url: string
    title: string
    description: string
    logo: string
}

const props = withDefaults(defineProps<Props>(), {
    url: '',
    title: '',
    description: '',
    logo: '',
})
</script>


<template>
    <div class="linkcard">
        <a :href="props.url" target="_blank">
            <p class="description">{{ props.title }}<br><span>{{ props.description }}</span></p>
            <div class="logo">
                <img alt="logo" width="70px" height="70px" :src="props.logo" />
            </div>
        </a>
    </div>
</template>

<style>
/* 卡片背景 */
.linkcard {
    background-color: var(--vp-c-bg-soft);
    border-radius: 8px;
    padding: 8px 16px 8px 8px;
    transition: color 0.5s, background-color 0.5s;
    margin-top: 15px;
}

/* 卡片鼠标悬停 */
.linkcard:hover {
    background-color: var(--vp-c-yellow-soft);
}

/* 链接样式 */
.linkcard a {
    display: flex;
    align-items: center;
}

/* 描述链接文字 */
.linkcard .description {
    flex: 1;
    font-weight: 500;
    font-size: 16px;
    line-height: 25px;
    color: var(--vp-c-text-1);
    margin: 0 0 0 16px;
    transition: color 0.5s;
}

/* 描述链接文字2 */
.linkcard .description span {
    font-size: 14px;
}

/* logo图片 */
.linkcard .logo img {
    width: 80px;
    object-fit: contain;
}

/* 链接下划线去除 */
.vp-doc a {
    text-decoration: none;
}
</style>

.vitepress\theme\index.js文件中注册全局组件。

js
import Linkcard from "./components/Linkcard.vue"

export default {
  extends: DefaultTheme,
  enhanceApp({app}) { 
    // 注册全局组件
    app.component('Linkcard' , Linkcard)  
  }
}

使用方式

markdown
<Linkcard url="网址" title="标题" description="描述" logo="logo图片路径"/>

比如:

<Linkcard url="https://yyg.js.cool/" title="云野阁" description="闲云野鹤,八方逍遥" logo="https://yyg.js.cool/img/icon.png"/>

实际效果:

参考链接:

标题颜色渐变

.vitepress\theme\style\var.css 文件中写入以下内容。

css
/************************* 标题颜色渐变 *************************/
h1 {
  background: -webkit-linear-gradient(10deg, #bd34fe 5%, #e43498 15%);
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

/************************* 标题颜色渐变 *************************/

img

侧边栏样式美化

.vitepress\theme\style\var.css 文件中写入以下内容。

css
/************************ 侧边栏美化 ************************/

/* 侧边栏缩放 */
.group:has([role='button']) .VPSidebarItem.level-0 .items {
  padding-left: 15px !important;
  border-left: 1px solid var(--vp-c-divider);
  border-radius: 2px;
  transition: background-color 0.25s;
}

/* 侧边栏图标 */
/* 选中所有 .VPSidebarItem 元素,排除带有 .is-link 类的 */
#VPSidebarNav .VPSidebarItem:not(.is-link).collapsed >.item {
    display: inline-flex;
    align-items: center;  /* 垂直居中对齐图标和文本 */
}

/* 为所有不带 .is-link 的 .VPSidebarItem 折叠状态添加图标 */
#VPSidebarNav .VPSidebarItem:not(.is-link).collapsed >.item::before {
    content: '';
    background-image: url('/img/folder.svg'); /* 设置图标路径 */
    width: 16px;
    height: 16px;
    display: inline-block;
    vertical-align: middle;  /* 确保图标与文本垂直居中 */
    background-size: cover;
    margin-right: 4px;  /* 给图标和文本之间增加间距 */
}

#VPSidebarNav .VPSidebarItem:not(.is-link) >.item {
    display: inline-flex;
    align-items: center;  /* 垂直居中对齐图标和文本 */
}

/* 为所有不带 .is-link 的 .VPSidebarItem 非折叠状态添加图标 */
#VPSidebarNav .VPSidebarItem:not(.is-link) >.item::before {
    content: '';
    background-image: url('/img/folder-open.svg'); /* 设置图标路径 */
    width: 16px;
    height: 16px;
    display: inline-block;
    vertical-align: middle;  /* 确保图标与文本垂直居中 */
    background-size: cover;
    margin-right: 4px;  /* 给图标和文本之间增加间距 */
}

/* 选中带有 .is-link 的 .VPSidebarItem 的直接子元素 .item */
#VPSidebarNav .VPSidebarItem.is-link > .item {
    display: inline-flex;
    align-items: center;  /* 垂直居中图标和文字 */
}

/* 为选中的 .item 添加图标 */
#VPSidebarNav .VPSidebarItem.is-link > .item::before {
    content: '';
    background-image: url('/img/file.svg'); /* 图标路径 */
    width: 16px;
    height: 16px;
    display: inline-block;
    vertical-align: middle;
    background-size: cover;
    margin-right: 4px;  /* 图标与文字间距 */
}

/************************ 侧边栏美化 ************************/

容器颜色

.vitepress\theme\style\var.css 文件中写入以下内容。

css
/************************* tab信息框容器颜色 *************************/
/* 深浅色卡 */
:root {
    --custom-block-info-left: #cccccc;
    --custom-block-info-bg: #fafafa;

    --custom-block-tip-left: #009400;
    --custom-block-tip-bg: #e6f6e6;

    --custom-block-warning-left: #e6a700;
    --custom-block-warning-bg: #fff8e6;

    --custom-block-danger-left: #e13238;
    --custom-block-danger-bg: #ffebec;

    --custom-block-note-left: #4cb3d4;
    --custom-block-note-bg: #eef9fd;

    --custom-block-important-left: #a371f7;
    --custom-block-important-bg: #f4eefe;

    --custom-block-caution-left: #e0575b;
    --custom-block-caution-bg: #fde4e8;
}

.dark {
    --custom-block-info-left: #cccccc;
    --custom-block-info-bg: #474748;

    --custom-block-tip-left: #009400;
    --custom-block-tip-bg: #003100;

    --custom-block-warning-left: #e6a700;
    --custom-block-warning-bg: #4d3800;

    --custom-block-danger-left: #e13238;
    --custom-block-danger-bg: #4b1113;

    --custom-block-note-left: #4cb3d4;
    --custom-block-note-bg: #193c47;

    --custom-block-important-left: #a371f7;
    --custom-block-important-bg: #230555;

    --custom-block-caution-left: #e0575b;
    --custom-block-caution-bg: #391c22;
}


/* 标题字体大小 */
.custom-block-title {
    font-size: 16px;
}

/* info容器:背景色、左侧 */
.custom-block.info {
    border-left: 5px solid var(--custom-block-info-left);
    background-color: var(--custom-block-info-bg);
}

/* info容器:svg图 */
.custom-block.info [class*="custom-block-title"]::before {
    content: '';
    background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-11v6h2v-6h-2zm0-4v2h2V7h-2z' fill='%23ccc'/%3E%3C/svg%3E");
    width: 20px;
    height: 20px;
    display: inline-block;
    vertical-align: middle;
    position: relative;
    margin-right: 4px;
    left: -5px;
    top: -1px;
}

/* 提示容器:边框色、背景色、左侧 */
.custom-block.tip {
    /* border-color: var(--custom-block-tip); */ 
    border-left: 5px solid var(--custom-block-tip-left);
    background-color: var(--custom-block-tip-bg);
}

/* 提示容器:svg图 */
.custom-block.tip [class*="custom-block-title"]::before {
    content: '';
    background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23009400' d='M7.941 18c-.297-1.273-1.637-2.314-2.187-3a8 8 0 1 1 12.49.002c-.55.685-1.888 1.726-2.185 2.998H7.94zM16 20v1a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-1h8zm-3-9.995V6l-4.5 6.005H11v4l4.5-6H13z'/%3E%3C/svg%3E");
    width: 20px;
    height: 20px;
    display: inline-block;
    vertical-align: middle;
    position: relative;
    margin-right: 4px;
    left: -5px;
    top: -2px;
}

/* 警告容器:背景色、左侧 */
.custom-block.warning {
    border-left: 5px solid var(--custom-block-warning-left);
    background-color: var(--custom-block-warning-bg);
}

/* 警告容器:svg图 */
.custom-block.warning [class*="custom-block-title"]::before {
    content: '';
    background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M576.286 752.57v-95.425q0-7.031-4.771-11.802t-11.3-4.772h-96.43q-6.528 0-11.3 4.772t-4.77 11.802v95.424q0 7.031 4.77 11.803t11.3 4.77h96.43q6.528 0 11.3-4.77t4.77-11.803zm-1.005-187.836 9.04-230.524q0-6.027-5.022-9.543-6.529-5.524-12.053-5.524H456.754q-5.524 0-12.053 5.524-5.022 3.516-5.022 10.547l8.538 229.52q0 5.023 5.022 8.287t12.053 3.265h92.913q7.032 0 11.803-3.265t5.273-8.287zM568.25 95.65l385.714 707.142q17.578 31.641-1.004 63.282-8.538 14.564-23.354 23.102t-31.892 8.538H126.286q-17.076 0-31.892-8.538T71.04 866.074q-18.582-31.641-1.004-63.282L455.75 95.65q8.538-15.57 23.605-24.61T512 62t32.645 9.04 23.605 24.61z' fill='%23e6a700'/%3E%3C/svg%3E");
    width: 20px;
    height: 20px;
    display: inline-block;
    vertical-align: middle;
    position: relative;
    margin-right: 4px;
    left: -5px;
}

/* 危险容器:背景色、左侧 */
.custom-block.danger {
    border-left: 5px solid var(--custom-block-danger-left);
    background-color: var(--custom-block-danger-bg);
}

/* 危险容器:svg图 */
.custom-block.danger [class*="custom-block-title"]::before {
    content: '';
    background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2c5.523 0 10 4.477 10 10v3.764a2 2 0 0 1-1.106 1.789L18 19v1a3 3 0 0 1-2.824 2.995L14.95 23a2.5 2.5 0 0 0 .044-.33L15 22.5V22a2 2 0 0 0-1.85-1.995L13 20h-2a2 2 0 0 0-1.995 1.85L9 22v.5c0 .171.017.339.05.5H9a3 3 0 0 1-3-3v-1l-2.894-1.447A2 2 0 0 1 2 15.763V12C2 6.477 6.477 2 12 2zm-4 9a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm8 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z' fill='%23e13238'/%3E%3C/svg%3E");
    width: 20px;
    height: 20px;
    display: inline-block;
    vertical-align: middle;
    position: relative;
    margin-right: 4px;
    left: -5px;
    top: -1px;
}

/* 提醒容器:背景色、左侧 */
.custom-block.note {
    border-left: 5px solid var(--custom-block-note-left);
    background-color: var(--custom-block-note-bg);
}

/* 提醒容器:svg图 */
.custom-block.note [class*="custom-block-title"]::before {
    content: '';
    background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-11v6h2v-6h-2zm0-4v2h2V7h-2z' fill='%234cb3d4'/%3E%3C/svg%3E");
    width: 20px;
    height: 20px;
    display: inline-block;
    vertical-align: middle;
    position: relative;
    margin-right: 4px;
    left: -5px;
    top: -1px;
}

/* 重要容器:背景色、左侧 */
.custom-block.important {
    border-left: 5px solid var(--custom-block-important-left);
    background-color: var(--custom-block-important-bg);
}

/* 重要容器:svg图 */
.custom-block.important [class*="custom-block-title"]::before {
    content: '';
    background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M512 981.333a84.992 84.992 0 0 1-84.907-84.906h169.814A84.992 84.992 0 0 1 512 981.333zm384-128H128v-42.666l85.333-85.334v-256A298.325 298.325 0 0 1 448 177.92V128a64 64 0 0 1 128 0v49.92a298.325 298.325 0 0 1 234.667 291.413v256L896 810.667v42.666zm-426.667-256v85.334h85.334v-85.334h-85.334zm0-256V512h85.334V341.333h-85.334z' fill='%23a371f7'/%3E%3C/svg%3E");
    width: 20px;
    height: 20px;
    display: inline-block;
    vertical-align: middle;
    position: relative;
    margin-right: 4px;
    left: -5px;
    top: -1px;
}

/* 注意容器:背景色、左侧 */
.custom-block.caution {
    border-left: 5px solid var(--custom-block-caution-left);
    background-color: var(--custom-block-caution-bg);
}

/* 注意容器:svg图 */
.custom-block.caution [class*="custom-block-title"]::before {
    content: '';
    background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2c5.523 0 10 4.477 10 10v3.764a2 2 0 0 1-1.106 1.789L18 19v1a3 3 0 0 1-2.824 2.995L14.95 23a2.5 2.5 0 0 0 .044-.33L15 22.5V22a2 2 0 0 0-1.85-1.995L13 20h-2a2 2 0 0 0-1.995 1.85L9 22v.5c0 .171.017.339.05.5H9a3 3 0 0 1-3-3v-1l-2.894-1.447A2 2 0 0 1 2 15.763V12C2 6.477 6.477 2 12 2zm-4 9a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm8 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z' fill='%23e13238'/%3E%3C/svg%3E");
    width: 20px;
    height: 20px;
    display: inline-block;
    vertical-align: middle;
    position: relative;
    margin-right: 4px;
    left: -5px;
    top: -1px;
}
/************************* tab信息框容器颜色 *************************/

首页特性悬停效果

.vitepress\theme\style\blur.css 文件中写入以下内容。

css
/************************ VPFeatures 页卡悬浮效果 ************************/
.VPFeatures .items .item {
  transition: transform 0.3s;
}

.VPFeatures .items .item:hover {
  transform: translateY(-5px);
}

    .VPFeature {
        border: 1px solid rgba(120, 122, 165, .25);
        box-shadow: 0 10px 30px 0 rgba(255, 255, 255, 0);
        background-color: transparent;
    }
}
/************************ VPFeatures 页卡悬浮效果 ************************/

导航栏毛玻璃

.vitepress\theme\style\blur.css 文件中写入以下内容。

css
/************************* 部件透明*************************/
:root {

    /* 首页下滑后导航透明 */
    .VPNavBar:not(.has-sidebar):not(.home.top) {
        background-color: rgba(255, 255, 255, 0);
        backdrop-filter: blur(10px);
    }


    /* Feature透明 */
    .VPFeature {
        border: 1px solid rgba(120, 122, 165, .25);
        box-shadow: 0 10px 30px 0 rgb(0 0 0 / 15%);
        background-color: transparent;
    }

    /* 文档页侧边栏顶部透明 */
    .curtain {
        background-color: rgba(255, 255, 255, 0);
        backdrop-filter: blur(10px);
    }

    @media (min-width: 960px) {

        /* 文档页导航中间透明 */
        .VPNavBar:not(.home.top) .content-body {
            background-color: rgba(255, 255, 255, 0);
            backdrop-filter: blur(10px);
        }
    }

    /* 移动端大纲栏透明 */
    .VPLocalNav {
        background-color: rgba(255, 255, 255, 0);
        backdrop-filter: blur(10px);
    }

}
/************************* 部件透明*************************/

返回顶部按钮

.vitepress\theme\components\backtotop.vue文件中加入以下内容。

vue
<script setup>
import { onBeforeUnmount, onMounted, ref, computed } from "vue";

const showBackTop = ref(false); // 初始状态设为false
const scrollProgress = ref(0);

// 圆形进度条计算
const radius = 42;
const circumference = computed(() => 2 * Math.PI * radius);

function scrollToTop() {
  window.scrollTo({
    top: 0,
    behavior: "smooth",
  });
}

// 使用更高效的节流函数
function throttle(fn, delay = 50) {
  let timer = null;
  return function(...args) {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, delay);
    }
  };
}

const updateScrollProgress = () => {
  const { scrollY, innerHeight } = window;
  const { scrollHeight } = document.documentElement;
  const totalScroll = scrollHeight - innerHeight;
  scrollProgress.value = totalScroll > 0 ? Math.min(scrollY / totalScroll, 1) : 0;
};

const handleScroll = throttle(() => {
  // 当滚动超过100px时显示,否则隐藏
  const shouldShow = window.scrollY > 100;
  showBackTop.value = shouldShow;
  updateScrollProgress();
});

onMounted(() => {
  window.addEventListener("scroll", handleScroll);
  updateScrollProgress();
});

onBeforeUnmount(() => {
  window.removeEventListener("scroll", handleScroll);
});
</script>

<template>
  <Transition name="fade">
    <div class="back-top-container" v-show="showBackTop">
      <svg class="progress-ring" viewBox="0 0 100 100">
        <circle class="progress-ring-background" cx="50" cy="50" r="42" />
        <circle 
          class="progress-ring-circle" 
          cx="50" 
          cy="50" 
          r="42"
          :style="{'stroke-dashoffset': circumference - (scrollProgress * circumference)}"
        />
      </svg>
      <div 
        class="vitepress-backTop-main" 
        title="返回顶部" 
        @click="scrollToTop()"
      >
        <svg class="icon" viewBox="0 0 1024 1024">
          <path d="M752.736 431.063C757.159 140.575 520.41 8.97 504.518 0.41V0l-0.45 0.205-0.41-0.205v0.41c-15.934 8.56-252.723 140.165-248.259 430.653-48.21 31.457-98.713 87.368-90.685 184.074 8.028 96.666 101.007 160.768 136.601 157.287 35.595-3.482 25.232-30.31 25.232-30.31l12.206-50.095s52.47 80.569 69.304 80.528c15.114-1.23 87-0.123 95.6 0h0.82c8.602-0.123 80.486-1.23 95.6 0 16.794 0 69.305-80.528 69.305-80.528l12.165 50.094s-10.322 26.83 25.272 30.31c35.595 3.482 128.574-60.62 136.602-157.286 8.028-96.665-42.475-152.617-90.685-184.074z m-248.669-4.26c-6.758-0.123-94.781-3.359-102.891-107.192 2.95-98.714 95.97-107.438 102.891-107.93 6.964 0.492 99.943 9.216 102.892 107.93-8.11 103.833-96.174 107.07-102.892 107.192z m-52.019 500.531c0 11.838-9.42 21.382-21.012 21.382a21.217 21.217 0 0 1-21.054-21.34V821.74c0-11.797 9.421-21.382 21.054-21.382 11.591 0 21.012 9.585 21.012 21.382v105.635z m77.333 57.222a21.504 21.504 0 0 1-21.34 21.626 21.504 21.504 0 0 1-21.34-21.626V827.474c0-11.96 9.543-21.668 21.299-21.668 11.796 0 21.38 9.708 21.38 21.668v157.082z m71.147-82.043c0 11.796-9.42 21.34-21.053 21.34a21.217 21.217 0 0 1-21.013-21.34v-75.367c0-11.755 9.421-21.299 21.013-21.299 11.632 0 21.053 9.544 21.053 21.3v75.366z" fill="#FFF"/>
        </svg>
      </div>
    </div>
  </Transition>
</template>

<style scoped>
.back-top-container {
  position: fixed;
  bottom: 20px;
  right: 20px;
  width: 60px;
  height: 60px;
  z-index: 999;
}

.vitepress-backTop-main {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  cursor: pointer;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background-color: #3eaf7c;
  padding: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
  transition: background-color 0.2s ease;
}

.vitepress-backTop-main:hover {
  background-color: #71cda3;
}

.progress-ring {
  position: absolute;
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
  z-index: 1;
}

.progress-ring-background {
  fill: none;
  stroke: rgba(62, 175, 124, 0.15);
  stroke-width: 3;
}

.progress-ring-circle {
  fill: none;
  stroke: #3eaf7c;
  stroke-width: 3;
  stroke-dasharray: 264; /* 2 * π * 42 */
  stroke-linecap: round;
  transition: stroke-dashoffset 0.15s ease-out;
}

.icon {
  width: 24px;
  height: 24px;
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

.vitepress\theme\index.js文件中加入以下内容。

js
import backtotop from "./components/backtotop.vue";    

export default {
  extends: DefaultTheme,
  Layout() {
    return h(DefaultTheme.Layout, null, {

      // 指定组件使用doc-footer-before插槽
      'doc-footer-before': () => h(backtotop),       

    })
  }
}

参考链接:

访客统计

提醒

访客统计的实现可选择使用 busuanzivercount统计服务

选择busuanzi需要下载busuanzi.pure.js,选择vercount可直接跳过这一步。

shell
npm install busuanzi.pure.js

.vitepress\theme\components\DataPanel.vue文件中写入以下内容。

vue
<template>
  <div class="panel">
    <div class="container">
      <section class="grid">
        <!-- busuanzi统计 - 访问量卡片 -->
        <div v-if="statsService === 'busuanzi'" class="card">
          <div class="text" id="busuanzi_container_site_pv">
            <div>本站总访问量</div>
            <span id="busuanzi_value_site_pv" class="font-bold">--</span>次
          </div>
        </div>
        
        <!-- Vercount统计 - 访问量卡片 -->
        <div v-if="statsService === 'vercount'" class="card">
          <div class="text" id="vercount_container_site_pv">
            <div>本站总访问量</div>
            <span id="vercount_value_site_pv" class="font-bold">--</span>次
          </div>
        </div>

        <!-- 心形图标卡片 -->
        <div class="card heart-card">
          <img src="/icon.png" alt="heart" class="heart-img" width="50" height="50" @click="onLinkUmiHandle" />
        </div>

        <!-- busuanzi统计 - 访客数卡片 -->
        <div v-if="statsService === 'busuanzi'" class="card">
          <div class="text" id="busuanzi_container_site_uv">
            <div>本站访客数</div>
            <span id="busuanzi_value_site_uv" class="font-bold">--</span>人次
          </div>
        </div>
        
        <!-- Vercount统计 - 访客数卡片 -->
        <div v-if="statsService === 'vercount'" class="card">
          <div class="text" id="vercount_container_site_uv">
            <div>本站访客数</div>
            <span id="vercount_value_site_uv" class="font-bold">--</span>人次
          </div>
        </div>
      </section>
    </div>
  </div>
</template>

<script setup lang="ts">
import { inBrowser } from "vitepress";
import { ref } from "vue";

// 配置项:选择使用的统计服务
// 可选值: 'busuanzi' | 'vercount'
const statsService = ref<'busuanzi' | 'vercount'>('busuanzi'); // 默认使用busuanzi

const onLinkUmiHandle = () => {
  if (inBrowser) {
    window.open(
      "", // 您可以在这里填入需要跳转的URL
      "_blank"
    );
  }
};
</script>

<style scoped>
.panel {
  margin-top: 12px;
  margin-bottom: 8px;
}

.container {
  width: 100%;
  max-width: 1152px;
  margin-left: auto;
  margin-right: auto;
}

.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
}

.card {
  background-color: var(--vp-c-bg-soft);
  border-radius: 8px;
  padding: 16px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  min-height: 100px;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.heart-card {
  cursor: pointer;
}

.heart-img {
  border-radius: 4px;
  transition: transform 0.2s ease;
}

.heart-card:hover .heart-img {
  transform: scale(1.1);
}

.text {
  font-size: 0.875rem;
  line-height: 1.25rem;
  text-align: center;
}

.font-bold {
  font-weight: bold;
  font-size: 1.25rem;
  margin-top: 4px;
  display: block;
}
</style>

注意

在DataPanel.vue中的标为高亮的一行,进行统计服务的选择,默认是busuanzi。

.vitepress\theme\index.js中写入以下内容。

js
import { inBrowser } from 'vitepress'
import busuanzi from 'busuanzi.pure.js'
import VisitorPanel from "./components/DataPanel.vue";    

  enhanceApp({ app, router, siteData }) {
    app.component("DataPanel", DataPanel);     
    if (inBrowser) {   
      // 路由加载完成,在加载页面组件后(在更新页面组件之前)调用。
      router.onAfterPageLoad = () => {   
        // 调用统计访问接口hooks
        useVisitData()  
      }  
    }  
}

在根目录的index.md最后加入以下内容。

markdown
<DataPanel />

img

NOTE

本地运行时,显示访问数值不准确为正常现象

参考原链接:

页脚版权

.vitepress\config.mjs文件中写入以下内容。

js
themeConfig: {
    // 页脚
    footer: {          
      message:   "Released under the MIT License.",     
      copyright: `Copyright © 2024-${new Date().getFullYear()} `, //这里可以写JS表达式
    },    
        
},

搜索样式美化

.vitepress\config.mjs文件中写入以下内容。

js
  themeConfig: {
   .....
    // 搜索
    search: {   
      provider: "local", 
    } 
      
  },

.vitepress\theme\style\var.css文件中加入以下内容。

css
/************************ 搜索框美化 ************************/
/* 搜索框的位置 */
.VPNavBarSearch.search {
  justify-content: flex-end !important;
  padding-right: 32px !important;
}

/* 搜索框透明 */
.DocSearch-Button {
  background-color: rgba(255, 255, 255, 0);
  backdrop-filter: blur(10px);
}


/* 鼠标悬停时的样式 */
.DocSearch-Button:hover {
   background: rgba(0,0,0,.2);
}
/************************ 搜索框美化 ************************/