Skip to content

配置站点信息

配置站点信息

image-20241226123237145

目录

[toc]

版权声明

主题选择

下面有两种配置方式可以选,分别为:

  • 在线主题:NPM 主题,采用监听路由、插入式的代码
  • 本地主题:站点信息模块与页面一起渲染出来,没有延迟

本地主题不好的一点就是版本升级后曾修改的内容被重置,所以需要记好修改位置、备份,比较麻烦。好处是根据自己的需求在基础上拓展。

在线主题具有通用性,即在任意环境(如本地主题)都有效果。

本次个人使用在线主题配置方式。😜

在线主题

建议:本内容代码块比较长,可以点击代码块的右侧箭头来折叠,然后点击复制图标进行复制即可。

不管使不使用本地主题,都可以配置在线主题的站点模块。

网站信息工具代码

添加网站信息需要的计算代码、获取字数代码等工具类。

首先进入 docs/.vuepress 目录,创建 webSiteInfo文件夹

image-20241226081318682

然后在 webSiteInfo 目录下创建 busuanzi.js文件,这个文件用于 获取访问量。

js
varbszCaller,bszTag,scriptTag,ready;vart,e,n,a =!1,c =[];if(typeofdocument !=="undefined") {(ready=function(t) {return(a ||"interactive"===document.readyState ||"complete"===document.readyState?t.call(document):c.push(function() {returnt.call(this);}),this);}),(e=function() {for(vart =0,e =c.length;t <e;t++) c[t].apply(document);c =[];}),(n=function() {a ||((a =!0),e.call(window),document.removeEventListener?document.removeEventListener("DOMContentLoaded",n,!1):document.attachEvent &&(document.detachEvent("onreadystatechange",n),window ==window.top &&(clearInterval(t),(t =null))));}),document.addEventListener?document.addEventListener("DOMContentLoaded",n,!1):document.attachEvent &&(document.attachEvent("onreadystatechange",function() {/loaded|complete/.test(document.readyState) &&n();}),window ==window.top &&(t =setInterval(function() {try{a ||document.documentElement.doScroll("left");} catch(t) {return;}n();},5)));}bszCaller ={fetch:function(t,e) {varn ="BusuanziCallback_"+Math.floor(1099511627776*Math.random());t =t.replace("=BusuanziCallback","="+n);(scriptTag =document.createElement("SCRIPT")),(scriptTag.type ="text/javascript"),(scriptTag.defer =!0),(scriptTag.src =t),document.getElementsByTagName("HEAD")[0].appendChild(scriptTag);window[n] =this.evalCall(e);},evalCall:function(e) {returnfunction(t) {ready(function() {try{e(t),scriptTag &&scriptTag.parentElement &&scriptTag.parentElement.removeChild &&scriptTag.parentElement.removeChild(scriptTag);} catch(t) {console.log(t),bszTag.hides();}});};},};bszTag ={bszs:["site_pv","page_pv","site_uv"],texts:function(n) {this.bszs.map(function(t) {vare =document.getElementById("busuanzi_value_"+t);e &&(e.innerHTML =n[t]);});},hides:function() {this.bszs.map(function(t) {vare =document.getElementById("busuanzi_container_"+t);e &&(e.style.display ="none");});},shows:function() {this.bszs.map(function(t) {vare =document.getElementById("busuanzi_container_"+t);e &&(e.style.display ="inline");});},};exportdefault() =>{bszTag &&bszTag.hides();bszCaller.fetch("bszTag.texts(t),bszTag.shows();})};

然后创建 readFile.js或者 readFile.ts文件,这个文件用于 统计文章数目 和 网站总字数 等。

添加如下内容:

注意:本次个人使用ts方式。

js

js
constfs=require('fs');constpath=require('path');constmatter=require('gray-matter');constchalk=require('chalk') constlog=console.logconstdocsRoot=path.join(__dirname,'..','..','..','docs');functionreadFileList(excludeFiles=[''],dir=docsRoot,filesList=[]) {constfiles=fs.readdirSync(dir);files.forEach((item,index) =>{letfilePath =path.join(dir,item);conststat=fs.statSync(filePath);if(!(excludeFiles instanceofArray)) {log(chalk.yellow(`error: 传入的参数不是一个数组。`))}excludeFiles.forEach((excludeFile) =>{if(stat.isDirectory() &&item !=='.vuepress'&&item !=='@pages'&&item !==excludeFile) {readFileList(excludeFiles,path.join(dir,item),filesList);} else{if(path.basename(dir) !=='docs') {constfileNameArr=path.basename(filePath).split('.')letname =null,type =null;if(fileNameArr.length===2) {name =fileNameArr[0]type =fileNameArr[1]} elseif(fileNameArr.length===3) {name =fileNameArr[1]type =fileNameArr[2]} else{log(chalk.yellow(`warning: 该文件 "${filePath}" 没有按照约定命名,将忽略生成相应数据。`))return}if(type ==='md') {filesList.push({name,filePath});}}}});});returnfilesList;}functionreadTotalFileWords(excludeFiles=['']) {constfilesList=readFileList(excludeFiles);varwordCount =0;filesList.forEach((item) =>{constcontent=getContent(item.filePath);varlen =counter(content);wordCount +=len[0] +len[1];});if(wordCount <1000) {returnwordCount;}returnMath.round(wordCount /100) /10+'k';}functionreadEachFileWords(excludeFiles=[''],cn,en) {constfilesListWords=[];constfilesList=readFileList(excludeFiles);filesList.forEach((item) =>{constcontent=getContent(item.filePath);varlen =counter(content);varreadingTime =readTime(len,cn,en);varwordsCount =0;wordsCount =len[0] +len[1];if(wordsCount >=1000) {wordsCount =Math.round(wordsCount /100) /10+'k';}constfileMatterObj=matter(content,{});constmatterData=fileMatterObj.data;filesListWords.push({...item,wordsCount,readingTime,...matterData });});returnfilesListWords;}functionreadTime(len,cn=300,en=160) {varreadingTime =len[0] /cn +len[1] /en;if(readingTime >60&&readingTime <60*24) {lethour =parseInt(readingTime /60);letminute =parseInt((readingTime -hour *60));if(minute ===0) {returnhour +'h';}returnhour +'h'+minute +'m';} elseif(readingTime >60*24) {letday =parseInt(readingTime /(60*24));lethour =parseInt((readingTime -day *24*60) /60);if(hour ===0) {returnday +'d';}returnday +'d'+hour +'h';}returnreadingTime <1?'1':parseInt((readingTime *10)) /10+'m';}functiongetContent(filePath) {returnfs.readFileSync(filePath,'utf8');}functioncounter(content) {constcn=(content.match(/[\u4E00-\u9FA5]/g) ||[]).length;consten=(content.replace(/[\u4E00-\u9FA5]/g,'').match(/[a-zA-Z0-9_\u0392-\u03c9\u0400-\u04FF]+|[\u4E00-\u9FFF\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af\u0400-\u04FF]+|[\u00E4\u00C4\u00E5\u00C5\u00F6\u00D6]+|\w+/g) ||[]).length;return[cn,en];}module.exports={readFileList,readTotalFileWords,readEachFileWords,}

ts

ts
importfs from'fs';importpath from'path';importmatter from'gray-matter';importchalk from'chalk'constlog=console.logconstdocsRoot=path.join(__dirname,'..','..','..','docs');functionreadFileList(excludeFiles:Array<string>=[''],dir:string=docsRoot,filesList:Array<Object>=[]) {constfiles=fs.readdirSync(dir);files.forEach((item,index) =>{letfilePath =path.join(dir,item);conststat=fs.statSync(filePath);if(!(excludeFiles instanceofArray)) {log(chalk.yellow(`error: 传入的参数不是一个数组。`))}excludeFiles.forEach((excludeFile) =>{if(stat.isDirectory() &&item !=='.vuepress'&&item !=='@pages'&&item !==excludeFile) {readFileList(excludeFiles,path.join(dir,item),filesList);} else{if(path.basename(dir) !=='docs') {constfileNameArr=path.basename(filePath).split('.')letname =null,type =null;if(fileNameArr.length===2) {name =fileNameArr[0]type =fileNameArr[1]} elseif(fileNameArr.length===3) {name =fileNameArr[1]type =fileNameArr[2]} else{log(chalk.yellow(`warning: 该文件 "${filePath}" 没有按照约定命名,将忽略生成相应数据。`))return}if(type ==='md') {filesList.push({name,filePath});}}}});});returnfilesList;}functionreadTotalFileWords(excludeFiles=['']) {constfilesList=readFileList(excludeFiles);letwordCount =0;filesList.forEach((item:any) =>{constcontent=getContent(item.filePath);letlen =counter(content);wordCount +=len[0] +len[1];});if(wordCount <1000) {returnwordCount;}returnMath.round(wordCount /100) /10+'k';}functionreadEachFileWords(excludeFiles:Array<string>=[''],cn:number,en:number) {constfilesListWords=[];constfilesList=readFileList(excludeFiles);filesList.forEach((item:any) =>{constcontent=getContent(item.filePath);letlen =counter(content);letreadingTime =readTime(len,cn,en);letwordsCount:any=0;wordsCount =len[0] +len[1];if(wordsCount >=1000) {wordsCount =Math.round(wordsCount /100) /10+'k';}constfileMatterObj=matter(content,{});constmatterData=fileMatterObj.data;filesListWords.push({...item,wordsCount,readingTime,...matterData });});returnfilesListWords;}functionreadTime(len:Array<number>,cn:number=300,en:number=160) {letreadingTime =len[0] /cn +len[1] /en;if(readingTime >60&&readingTime <60*24) {lethour =Math.trunc(readingTime /60);letminute =Math.trunc(readingTime -hour *60);if(minute ===0) {returnhour +'h';}returnhour +'h'+minute +'m';} elseif(readingTime >60*24) {letday =Math.trunc(readingTime /(60*24));lethour =Math.trunc((readingTime -day *24*60) /60);if(hour ===0) {returnday +'d';}returnday +'d'+hour +'h';}returnreadingTime <1?'1':Math.trunc(readingTime *10) /10+'m';}functiongetContent(filePath:string) {returnfs.readFileSync(filePath,'utf8');}functioncounter(content:string) {constcn=(content.match(/[\u4E00-\u9FA5]/g) ||[]).length;consten=(content.replace(/[\u4E00-\u9FA5]/g,'').match(/[a-zA-Z0-9_\u0392-\u03c9\u0400-\u04FF]+|[\u4E00-\u9FFF\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af\u0400-\u04FF]+|[\u00E4\u00C4\u00E5\u00C5\u00F6\u00D6]+|\w+/g) ||[]).length;return[cn,en];}export{readFileList,readTotalFileWords,readEachFileWords,}

接着继续在该目录下创建第三个文件 utils.js,该文件用于计算 已运行时间 和 最后活动时间。

添加如下内容:

js
exportfunctiondateFormat(date) {if(!(date instanceofDate)) {date =newDate(date);}return`${date.getUTCFullYear()}-${zero(date.getUTCMonth() + 1)}-${zero(date.getUTCDate())}`;}exportfunctionzero(d) {returnd.toString().padStart(2,'0');}exportfunctionlastUpdatePosts(posts) {posts.sort((prev,next) =>{returncompareDate(prev,next);});returnposts;}exportfunctiongetTimeNum(post) {letdateStr =post.lastUpdated ||post.frontmatter.date;letdate =newDate(dateStr);if(date =="Invalid Date"&&dateStr) {date =newDate(dateStr.replace(/-/g,'/'));}returndate.getTime();}exportfunctioncompareDate(a,b) {returngetTimeNum(b) -getTimeNum(a);}exportfunctiondayDiff(startDate,endDate) {if(!endDate) {endDate =startDate;startDate =newDate();}startDate =dateFormat(startDate);endDate =dateFormat(endDate);letday =parseInt(Math.abs(newDate(startDate) -newDate(endDate)) /(1000*60*60*24));returnday;}exportfunctiontimeDiff(startDate,endDate) {if(!endDate) {endDate =startDate;startDate =newDate();}if(!(startDate instanceofDate)) {startDate =newDate(startDate);}if(!(endDate instanceofDate)) {endDate =newDate(endDate);}constdiffValue=parseInt((Math.abs(endDate -startDate) /1000));if(diffValue ==0) {return'刚刚';} elseif(diffValue <60) {returndiffValue +'秒';} elseif(parseInt(diffValue /60) <60) {returnparseInt(diffValue /60) +'分';} elseif(parseInt(diffValue /(60*60)) <24) {returnparseInt(diffValue /(60*60)) +'时';} elseif(parseInt(diffValue /(60*60*24)) <getDays(startDate.getMonth,startDate.getFullYear)) {returnparseInt(diffValue /(60*60*24)) +'天';} elseif(parseInt(diffValue /(60*60*24*getDays(startDate.getMonth,startDate.getFullYear))) <12) {returnparseInt(diffValue /(60*60*24*getDays(startDate.getMonth,startDate.getFullYear))) +'月';} else{returnparseInt(diffValue /(60*60*24*getDays(startDate.getMonth,startDate.getFullYear) *12)) +'年';}}exportfunctiongetDays(mouth,year) {letdays =30;if(mouth ===2) {days =year %4===0?29:28;} elseif(mouth ===1||mouth ===3||mouth ===5||mouth ===7||mouth ===8||mouth ===10||mouth ===12) {days =31;}returndays;}

目前就三个文件,最终效果如图:

image-20241226081530171

站点信息代码

这一步的文件目录不能随便移动,因为该目录是 Vuepress 规定的。

首先进入 docs/.vuepress 目录,创建 components 文件夹

image-20241226081612173

创建一个 vue 文件:WebInfo.vue,这就是首页的站点信息模块。

并添加如下内容:

vue
<template><!-- Young Kbt --><divclass="web-info card-box"><divclass="webinfo-title"><iclass="iconfont icon-award"style="font-size:0.875rem;font-weight:900;width:1.25em"></i><span>站点信息</span></div><divclass="webinfo-item"><divclass="webinfo-item-title">文章数目:</div><divclass="webinfo-content">{{mdFileCount }} 篇</div></div><divclass="webinfo-item"><divclass="webinfo-item-title">已运行时间:</div><divclass="webinfo-content">{{createToNowDay !=0 ?createToNowDay + "天":"不到一天"}}</div></div><divclass="webinfo-item"><divclass="webinfo-item-title">本站总字数:</div><divclass="webinfo-content">{{totalWords }} 字</div></div><divclass="webinfo-item"><divclass="webinfo-item-title">最后活动时间:</div><divclass="webinfo-content">{{lastActiveDate =="刚刚"?"刚刚":lastActiveDate + "前"}}</div></div><divv-if="indexView"class="webinfo-item"><divclass="webinfo-item-title">本站被访问了:</div><divclass="webinfo-content"><spanid="busuanzi_value_site_pv"class="web-site-pv"><ititle="正在获取..."class="loading iconfont icon-loading"></i></span></div></div><divv-if="indexView"class="webinfo-item"><divclass="webinfo-item-title">您的访问排名:</div><divclass="webinfo-content busuanzi"><spanid="busuanzi_value_site_uv"class="web-site-uv"><ititle="正在获取..."class="loading iconfont icon-loading"></i></span></div></div></div></template><script>import{dayDiff,timeDiff,lastUpdatePosts } from"../webSiteInfo/utils";importfetch from"../webSiteInfo/busuanzi";exportdefault{data() {return{mdFileCount:0,createToNowDay:0,lastActiveDate:"",totalWords:0,indexView:true,};},computed:{$lastUpdatePosts() {returnlastUpdatePosts(this.$filterPosts);},},mounted() {if(Object.keys(this.$themeConfig.blogInfo).length>0) {const{blogCreate,mdFileCountType,totalWords,moutedEvent,eachFileWords,indexIteration,indexView,} =this.$themeConfig.blogInfo;this.createToNowDay =dayDiff(blogCreate);if(mdFileCountType !="archives") {this.mdFileCount =mdFileCountType.length;} else{this.mdFileCount =this.$filterPosts.length;}if(totalWords =="archives"&&eachFileWords) {letarchivesWords =0;eachFileWords.forEach((itemFile) =>{if(itemFile.wordsCount <1000) {archivesWords +=itemFile.wordsCount;} else{letwordsCount =itemFile.wordsCount.slice(0,itemFile.wordsCount.length-1);archivesWords +=wordsCount *1000;}});this.totalWords =Math.round(archivesWords /100) /10+"k";} elseif(totalWords =="archives") {this.totalWords =0;console.log("如果 totalWords ='archives',必须传入 eachFileWords,显然您并没有传入!");} else{this.totalWords =totalWords;}this.lastActiveDate =timeDiff(this.$lastUpdatePosts[0].lastUpdated);this.mountedWebInfo(moutedEvent);this.indexView =indexView ==undefined?true:indexView;if(this.indexView) {this.getIndexViewCouter(indexIteration);}}},methods:{mountedWebInfo(moutedEvent=".tags-wrapper") {letinterval =setInterval(() =>{consttagsWrapper=document.querySelector(moutedEvent);constwebInfo=document.querySelector(".web-info");if(tagsWrapper &&webInfo) {if(!this.isSiblilngNode(tagsWrapper,webInfo)) {tagsWrapper.parentNode.insertBefore(webInfo,tagsWrapper.nextSibling);clearInterval(interval);}}},200);},isSiblilngNode(element,siblingNode) {if(element.siblingNode ==siblingNode) {returntrue;} else{returnfalse;}},getIndexViewCouter(iterationTime=3000) {fetch();vari =0;vardefaultCouter ="9999";setTimeout(() =>{letindexUv =document.querySelector(".web-site-pv");letindexPv =document.querySelector(".web-site-uv");if(indexPv &&indexUv &&indexPv.innerText ==""&&indexUv.innerText =="") {letinterval =setInterval(() =>{if(indexPv &&indexUv &&indexPv.innerText ==""&&indexUv.innerText =="") {i +=iterationTime;if(i >iterationTime *5) {indexPv.innerText =defaultCouter;indexUv.innerText =defaultCouter;clearInterval(interval);}if(indexPv.innerText ==""&&indexUv.innerText =="") {fetch();} else{clearInterval(interval);}} else{clearInterval(interval);}},iterationTime);this.$once("hook:beforeDestroy",() =>{clearInterval(interval);interval =null;});}},iterationTime);},beforeMount() {letwebInfo =document.querySelector(".web-info");webInfo &&webInfo.parentNode.removeChild(webInfo);},},};</script><stylescoped>.web-info{font-size:0.875rem;padding:0.95rem;}.webinfo-title{text-align:center;color:#888;font-weight:bold;padding:0010px0;}.webinfo-item{padding:8px00;margin:0;}.webinfo-item-title{display:inline-block;}.webinfo-content{display:inline-block;float:right;}@keyframesturn{0%{transform:rotate(0deg);}100%{transform:rotate(360deg);}}.loading{display:inline-block;animation:turn 1slinearinfinite;-webkit-animation:turn 1slinearinfinite;}</style>

继续创建一个 vue 文件:PageInfo.vue,这就是文章页的信息模块:文章浏览量、字数代码、预阅读时间。

vue
<template></template><script>importfetch from"../webSiteInfo/busuanzi";exportdefault{mounted() {if(this.$route.path !="/") {this.initPageInfo();}},watch:{$route(to,from) {if(to.path !=="/"&&to.path !==from.path &&this.$themeConfig.blogInfo) {this.initPageInfo();}},},methods:{initPageInfo() {if(this.$frontmatter.article ==undefined||this.$frontmatter.article) {const{eachFileWords,pageView,pageIteration,readingTime} =this.$themeConfig.blogInfo;if(eachFileWords) {try{eachFileWords.forEach((itemFile) =>{if(itemFile.permalink ==this.$frontmatter.permalink) {this.addPageWordsCount(itemFile.wordsCount);if(readingTime ||readingTime ==undefined) {this.addReadTimeCount(itemFile.readingTime);}thrownewError();}});} catch(error) {}}if(pageView ||pageView ==undefined) {this.addPageView();this.getPageViewCouter(pageIteration);}return;}},getPageViewCouter(iterationTime=3000) {fetch();leti =0;vardefaultCouter ="9999";setTimeout(() =>{letpageView =document.querySelector(".view-data");if(pageView &&pageView.innerText =="") {letinterval =setInterval(() =>{if(pageView &&pageView.innerText =="") {i +=iterationTime;if(i >iterationTime *5) {pageView.innerText =defaultCouter;clearInterval(interval);}if(pageView.innerText =="") {fetch();} else{clearInterval(interval);}} else{clearInterval(interval);}},iterationTime);this.$once("hook:beforeDestroy",() =>{clearInterval(interval);interval =null;});}},iterationTime);},addPageView() {letpageView =document.querySelector(".page-view");if(pageView) {pageView.innerHTML ='<a style="color:#888;margin-left:3px"href="javascript:;"id="busuanzi_value_page_pv"class="view-data"><i title="正在获取..."class="loading iconfont icon-loading"></i></a>';} else{lettemplate =document.createElement("div");template.title ="浏览量";template.className ="page-view iconfont icon-view";template.style.float ="left";template.style.marginLeft ="20px";template.style.fontSize ="0.8rem";template.innerHTML ='<a style="color:#888;margin-left:3px"href="javascript:;"id="busuanzi_value_page_pv"class="view-data"><i title="正在获取..."class="loading iconfont icon-loading"></i></a>';letstyle =document.createElement("style");style.innerHTML =`@keyframes turn {
        0% {
          transform: rotate(0deg);
        }
        100% {
          transform: rotate(360deg);
        }
      }
      .loading {
        display: inline-block;
        animation: turn 1s linear infinite;
        -webkit-animation: turn 1s linear infinite;
      }`;document.head.appendChild(style);this.mountedView(template);}},addPageWordsCount(wordsCount=0) {letwords =document.querySelector(".book-words");if(words) {words.innerHTML =`<a href="javascript:;" style="margin-left: 3px; color: #888">${wordsCount}</a>`;} else{lettemplate =document.createElement("div");template.title ="文章字数";template.className ="book-words iconfont icon-book";template.style.float ="left";template.style.marginLeft ="20px";template.style.fontSize ="0.8rem";template.innerHTML =`<a href="javascript:;" style="margin-left: 3px; color: #888">${wordsCount}</a>`;this.mountedView(template);}},addReadTimeCount(readTimeCount=0) {letreading =document.querySelector(".reading-time");if(reading) {reading.innerHTML =`<a href="javascript:;" style="margin-left: 3px; color: #888">${readTimeCount}</a>`;} else{lettemplate =document.createElement("div");template.title ="预阅读时长";template.className ="reading-time iconfont icon-shijian";template.style.float ="left";template.style.marginLeft ="20px";template.style.fontSize ="0.8rem";template.innerHTML =`<a href="javascript:;" style="margin-left: 3px; color: #888">${readTimeCount}</a>`;this.mountedView(template);}},mountedView(template,mountedIntervalTime=100,moutedParentEvent=".articleInfo-wrap >.articleInfo >.info") {leti =0;letparentElement =document.querySelector(moutedParentEvent);if(parentElement) {if(!this.isMountedView(template,parentElement)) {parentElement.appendChild(template);}} else{letinterval =setInterval(() =>{parentElement =document.querySelector(moutedParentEvent);if(parentElement) {if(!this.isMountedView(template,parentElement)) {parentElement.appendChild(template);clearInterval(interval);}} elseif(i >1*10) {clearInterval(interval);}},mountedIntervalTime);this.$once("hook:beforeDestroy",() =>{clearInterval(interval);interval =null;});}},removeElement(selector) {varelement =document.querySelector(selector);element &&element.parentNode.removeChild(element);},isMountedView(element,parentElement) {if(element.parentNode ==parentElement) {returntrue;} else{returnfalse;}},},beforeMount() {clearInterval(this.interval);this.removeElement(".page-view");this.removeElement(".book-words");this.removeElement(".reading-time");},};</script><style></style>

最终效果如图:

image-20241226081735510

创建好了两个 vue 组件,我们需要使用它们。

使用 WebInfo.vue组件

打开 docs/index.md

image-20241226081752568

移到最下方,添加如下内容:

vue
<ClientOnly><WebInfo/></ClientOnly>

使用 PageInfo.vue组件

在 docs/.vuepress/config.js(新版是 config.ts)的 plugins 中添加配置。

js

js
module.exports={plugins:[{name:'custom-plugins',globalUIComponents:["PageInfo"] }]}

ts (本次使用这个)

ts
import{UserPlugins } from'vuepress/config'plugins:<UserPlugins>[[{name:'custom-plugins',globalUIComponents:["PageInfo"] }]]

站点信息配置

上面都按照步骤写好代码、使用组件了,那么就可以走最后一步配置我们的站点信息。

进入到 docs/.vuepress/config.js(新版为 config.ts)文件。

引入之前写好的工具代码文件:(路径要准确,这里仅仅是模板)

js

js
const{readFileList,readTotalFileWords,readEachFileWords} =require('./webSiteInfo/readFile');

ts (本次)

ts
import{readFileList,readTotalFileWords,readEachFileWords } from'./webSiteInfo/readFile';

如图(演示 JS 代码块):

image-20241226081918081

在 themeConfig 中添加如下内容:

js
blogInfo:{blogCreate:'2021-10-19',indexView:true,pageView:true,readingTime:true,eachFileWords:readEachFileWords([''],300,160),mdFileCountType:'archives',totalWords:'archives',moutedEvent:'.tags-wrapper',indexIteration:2500,pageIteration:2500,},

如图(图片内容不一定是最新,最新的是代码块内容):

image-20241226081952327

属性配置的具体介绍请看 属性配置

本地主题

如果已经看完了在线主题的内容,其实本内容的大小不变,只是位置变换、一些代码重组。

配置了在线主题,就不需要配置本地主题,反之亦然。

工具类

在 vdoing/util 目录下创建 webSiteInfo.js,添加如下内容:

js
exportfunctiondateFormat(date) {if(!(date instanceofDate)) {date =newDate(date);}return`${date.getUTCFullYear()}-${zero(date.getUTCMonth() + 1)}-${zero(date.getUTCDate())}`;}exportfunctionzero(d) {returnd.toString().padStart(2,'0');}exportfunctionlastUpdatePosts(posts) {posts.sort((prev,next) =>{returncompareDate(prev,next);});returnposts;}exportfunctiongetTimeNum(post) {letdateStr =post.lastUpdated ||post.frontmatter.date;letdate =newDate(dateStr);if(date =="Invalid Date"&&dateStr) {date =newDate(dateStr.replace(/-/g,'/'));}returndate.getTime();}exportfunctioncompareDate(a,b) {returngetTimeNum(b) -getTimeNum(a);}exportfunctiondayDiff(startDate,endDate) {if(!endDate) {endDate =startDate;startDate =newDate();}startDate =dateFormat(startDate);endDate =dateFormat(endDate);letday =parseInt(Math.abs(newDate(startDate) -newDate(endDate)) /(1000*60*60*24));returnday;}exportfunctiontimeDiff(startDate,endDate) {if(!endDate) {endDate =startDate;startDate =newDate();}if(!(startDate instanceofDate)) {startDate =newDate(startDate);}if(!(endDate instanceofDate)) {endDate =newDate(endDate);}constdiffValue=parseInt((Math.abs(endDate -startDate) /1000));if(diffValue ==0) {return'刚刚';} elseif(diffValue <60) {returndiffValue +'秒';} elseif(parseInt(diffValue /60) <60) {returnparseInt(diffValue /60) +'分';} elseif(parseInt(diffValue /(60*60)) <24) {returnparseInt(diffValue /(60*60)) +'时';} elseif(parseInt(diffValue /(60*60*24)) <getDays(startDate.getMonth,startDate.getFullYear)) {returnparseInt(diffValue /(60*60*24)) +'天';} elseif(parseInt(diffValue /(60*60*24*getDays(startDate.getMonth,startDate.getFullYear))) <12) {returnparseInt(diffValue /(60*60*24*getDays(startDate.getMonth,startDate.getFullYear))) +'月';} else{returnparseInt(diffValue /(60*60*24*getDays(startDate.getMonth,startDate.getFullYear) *12)) +'年';}}exportfunctiongetDays(mouth,year) {letdays =30;if(mouth ===2) {days =year %4===0?29:28;} elseif(mouth ===1||mouth ===3||mouth ===5||mouth ===7||mouth ===8||mouth ===10||mouth ===12) {days =31;}returndays;}exportfunctiongetTime(startDate,endDate) {if(day <0) {lethour =parseInt(Math.abs(newDate(startDate) -newDate(endDate)) /(1000*60*60));if(hour >0) {letminute =parseInt(Math.abs(newDate(startDate) -newDate(endDate) -hour *60*60*1000) /(1000*60));if(minute >0) {letsecond =parseInt(Math.abs(newDate(startDate) -newDate(endDate) -hour *60*60*1000-minute *60*1000) /(1000));if(second !=0) {returnhour +'小时 '+minute +'分钟 '+second +'秒';} else{returnhour +'小时 '+minute +'分钟 ';}} else{returnhour +'小时 ';}} else{letminute =parseInt(Math.abs(newDate(startDate) -newDate(endDate) -hour *60*60*1000) /(1000*60));if(minute >0) {letsecond =parseInt(Math.abs(newDate(startDate) -newDate(endDate) -hour *60*60*1000-minute *60*1000) /(1000));if(second !=0) {return+minute +'分钟 '+second +'秒';} else{returnminute +'分钟 ';}} else{returnparseInt(Math.abs(newDate(startDate) -newDate(endDate) -hour *60*60*1000-minute *60*1000) /(1000)) +'秒 ';}}}}varbszCaller,bszTag,scriptTag,ready;vart,e,n,a =!1,c =[];if(typeofdocument !=="undefined") {(ready=function(t) {return(a ||"interactive"===document.readyState ||"complete"===document.readyState?t.call(document):c.push(function() {returnt.call(this);}),this);}),(e=function() {for(vart =0,e =c.length;t <e;t++) c[t].apply(document);c =[];}),(n=function() {a ||((a =!0),e.call(window),document.removeEventListener?document.removeEventListener("DOMContentLoaded",n,!1):document.attachEvent &&(document.detachEvent("onreadystatechange",n),window ==window.top &&(clearInterval(t),(t =null))));}),document.addEventListener?document.addEventListener("DOMContentLoaded",n,!1):document.attachEvent &&(document.attachEvent("onreadystatechange",function() {/loaded|complete/.test(document.readyState) &&n();}),window ==window.top &&(t =setInterval(function() {try{a ||document.documentElement.doScroll("left");} catch(t) {return;}n();},5)));}bszCaller ={fetch:function(t,e) {varn ="BusuanziCallback_"+Math.floor(1099511627776*Math.random());t =t.replace("=BusuanziCallback","="+n);(scriptTag =document.createElement("SCRIPT")),(scriptTag.type ="text/javascript"),(scriptTag.defer =!0),(scriptTag.src =t),document.getElementsByTagName("HEAD")[0].appendChild(scriptTag);window[n] =this.evalCall(e);},evalCall:function(e) {returnfunction(t) {ready(function() {try{e(t),scriptTag &&scriptTag.parentElement &&scriptTag.parentElement.removeChild &&scriptTag.parentElement.removeChild(scriptTag);} catch(t) {console.log(t),bszTag.hides();}});};},};exportfunctionfetch() {bszTag &&bszTag.hides();bszCaller.fetch("bszTag.texts(t),bszTag.shows();})};bszTag ={bszs:["site_pv","page_pv","site_uv"],texts:function(n) {this.bszs.map(function(t) {vare =document.getElementById("busuanzi_value_"+t);e &&(e.innerHTML =n[t]);});},hides:function() {this.bszs.map(function(t) {vare =document.getElementById("busuanzi_container_"+t);e &&(e.style.display ="none");});},shows:function() {this.bszs.map(function(t) {vare =document.getElementById("busuanzi_container_"+t);e &&(e.style.display ="inline");});},};

Vue组件创建

需要两个 Vue 组件,分别是首页的站点信息模块和文章页信息模块。

在 vdoing/components 目录下创建 WebInfo.vue文件,添加如下内容:

vue
<template><!-- Young Kbt --><divclass="web-info card-box"><divclass="webinfo-title"><iclass="iconfont icon-award"style="font-size:0.875rem;font-weight:900;width:1.25em"></i><span>站点信息</span></div><divclass="webinfo-item"><divclass="webinfo-item-title">文章数目:</div><divclass="webinfo-content">{{mdFileCount }} 篇</div></div><divclass="webinfo-item"><divclass="webinfo-item-title">已运行时间:</div><divclass="webinfo-content">{{createToNowDay !=0 ?createToNowDay + "天":"不到一天"}}</div></div><divclass="webinfo-item"><divclass="webinfo-item-title">本站总字数:</div><divclass="webinfo-content">{{totalWords }} 字</div></div><divclass="webinfo-item"><divclass="webinfo-item-title">最后活动时间:</div><divclass="webinfo-content">{{lastActiveDate =="刚刚"?"刚刚":lastActiveDate + "前"}}</div></div><divv-if="indexView"class="webinfo-item"><divclass="webinfo-item-title">本站被访问了:</div><divclass="webinfo-content"><spanid="busuanzi_value_site_pv"class="web-site-pv"><ititle="正在获取..."class="loading iconfont icon-loading"></i></span></div></div><divv-if="indexView"class="webinfo-item"><divclass="webinfo-item-title">您的访问排名:</div><divclass="webinfo-content busuanzi"><spanid="busuanzi_value_site_uv"class="web-site-uv"><ititle="正在获取..."class="loading iconfont icon-loading"></i></span></div></div></div></template><script>import{dayDiff,timeDiff,lastUpdatePosts,fetch } from"../util/webSiteInfo";exportdefault{data() {return{mdFileCount:0,createToNowDay:0,lastActiveDate:"",totalWords:0,indexView:true,};},computed:{$lastUpdatePosts() {returnlastUpdatePosts(this.$filterPosts);},},mounted() {if(Object.keys(this.$themeConfig.blogInfo).length>0) {const{blogCreate,mdFileCountType,totalWords,moutedEvent,eachFileWords,indexIteration,indexView,} =this.$themeConfig.blogInfo;this.createToNowDay =dayDiff(blogCreate);if(mdFileCountType !="archives") {this.mdFileCount =mdFileCountType.length;} else{this.mdFileCount =this.$filterPosts.length;}if(totalWords =="archives"&&eachFileWords) {letarchivesWords =0;eachFileWords.forEach((itemFile) =>{if(itemFile.wordsCount <1000) {archivesWords +=itemFile.wordsCount;} else{letwordsCount =itemFile.wordsCount.slice(0,itemFile.wordsCount.length-1);archivesWords +=wordsCount *1000;}});this.totalWords =Math.round(archivesWords /100) /10+"k";} elseif(totalWords =="archives") {this.totalWords =0;console.log("如果 totalWords ='archives',必须传入 eachFileWords,显然您并没有传入!");} else{this.totalWords =totalWords;}this.lastActiveDate =timeDiff(this.$lastUpdatePosts[0].lastUpdated);this.mountedWebInfo(moutedEvent);this.indexView =indexView ==undefined?true:indexView;if(this.indexView) {this.getIndexViewCouter(indexIteration);}}},methods:{mountedWebInfo(moutedEvent=".tags-wrapper") {letinterval =setInterval(() =>{consttagsWrapper=document.querySelector(moutedEvent);constwebInfo=document.querySelector(".web-info");if(tagsWrapper &&webInfo) {if(!this.isSiblilngNode(tagsWrapper,webInfo)) {tagsWrapper.parentNode.insertBefore(webInfo,tagsWrapper.nextSibling);clearInterval(interval);}}},200);},isSiblilngNode(element,siblingNode) {if(element.siblingNode ==siblingNode) {returntrue;} else{returnfalse;}},getIndexViewCouter(iterationTime=3000) {fetch();vari =0;vardefaultCouter ="9999";setTimeout(() =>{letindexUv =document.querySelector(".web-site-pv");letindexPv =document.querySelector(".web-site-uv");if(indexPv &&indexUv &&indexPv.innerText ==""&&indexUv.innerText =="") {letinterval =setInterval(() =>{if(indexPv &&indexUv &&indexPv.innerText ==""&&indexUv.innerText =="") {i +=iterationTime;if(i >iterationTime *5) {indexPv.innerText =defaultCouter;indexUv.innerText =defaultCouter;clearInterval(interval);}if(indexPv.innerText ==""&&indexUv.innerText =="") {fetch();} else{clearInterval(interval);}} else{clearInterval(interval);}},iterationTime);this.$once("hook:beforeDestroy",() =>{clearInterval(interval);interval =null;});}},iterationTime);},},};</script><stylescoped>.web-info{font-size:0.875rem;padding:0.95rem;}.webinfo-title{text-align:center;color:#888;font-weight:bold;padding:0010px0;}.webinfo-item{padding:8px00;margin:0;}.webinfo-item-title{display:inline-block;}.webinfo-content{display:inline-block;float:right;}@keyframesturn{0%{transform:rotate(0deg);}100%{transform:rotate(360deg);}}.loading{display:inline-block;animation:turn 1slinearinfinite;-webkit-animation:turn 1slinearinfinite;}</style>

继续在 vdoing/components 目录下创建 PageInfo.vue文件,添加如下内容:

vue
<template><divclass="page-view"><!-- 文章字数 --><divtitle="文章字数"class="book-words iconfont icon-book"><ahref="javascript:;"style="margin-left:3px;color:#888">{{wordsCount}}</a></div><!-- 预阅读时长 --><divv-if="readingTime"title="预阅读时长"class="reading-time iconfont icon-shijian"><ahref="javascript:;"style="margin-left:3px;color:#888">{{readingTime}}</a></div><!-- 浏览量 --><divv-if="pageView"title="浏览量"class="page-view iconfont icon-view"><astyle="color:#888;margin-left:3px"href="javascript:;"id="busuanzi_value_page_pv"class="view-data"><ititle="正在获取..."class="loading iconfont icon-loading"></i></a></div></div></template><script>import{fetch } from"../util/webSiteInfo";exportdefault{data() {return{wordsCount:0,readingTime:0,pageView:true,pageIteration:3000,};},mounted() {this.initPageInfo();},watch:{$route(to,from) {if(to.path !=="/"&&to.path !=from.path &&this.$themeConfig.blogInfo) {this.initPageInfo();}},},methods:{initPageInfo() {this.$filterPosts.forEach((itemPage) =>{if(itemPage.path ==this.$route.path) {const{eachFileWords,pageView,pageIteration,readingTime} =this.$themeConfig.blogInfo;this.pageIteration =pageIteration;if(eachFileWords) {eachFileWords.forEach((itemFile) =>{if(itemFile.permalink ==itemPage.frontmatter.permalink) {this.wordsCount =itemFile.wordsCount;if(readingTime ||readingTime ==undefined) {this.readingTime =itemFile.readingTime;} else{this.readingTime =false;}}});}this.pageView =pageView ==undefined?true:pageView;if(this.pageView) {this.getPageViewCouter(this.pageIteration);}return;}});},getPageViewCouter(iterationTime=3000) {fetch();leti =0;vardefaultCouter ="9999";setTimeout(() =>{letpageView =document.querySelector(".view-data");if(pageView &&pageView.innerText =="") {letinterval =setInterval(() =>{if(pageView &&pageView.innerText =="") {i +=iterationTime;if(i >iterationTime *5) {pageView.innerText =defaultCouter;clearInterval(interval);}if(pageView.innerText =="") {fetch();} else{clearInterval(interval);}} else{clearInterval(interval);}},iterationTime);this.$once("hook:beforeDestroy",() =>{clearInterval(interval);interval =null;});}},iterationTime);},},};</script><stylescoped>.page-view>div{float:left;margin-left:20px;font-size:0.8rem;}@keyframesturn{0%{transform:rotate(0deg);}100%{transform:rotate(360deg);}}.loading{display:inline-block;animation:turn 1slinearinfinite;-webkit-animation:turn 1slinearinfinite;}</style>

Vue组件引用

写好两个组件,那么我们需要使用它们。

引入 WebInfo.vue组件

打开 vdoing/components/Home.vue文件。

大概在 174 行处引入 WebInfo.vue组件:

js
importWebInfo from'./WebInfo.vue';

1

大概在 242 行处找到 components注册该组件:

js
components:{......,WebInfo },

1

大概在 153 行处(div 的 class 为 custom-html-box的上方),添加如下内容:

js
<webInfo/>

1

三个效果图:

image-20241226124321368

引入 PageInfo.vue组件

打开 vdoing/components/ArticleInfo.vue文件。

大概在 67 行处引入 PagesView.vue组件:

js
importPageInfo from'./PageInfo.vue';

1

大概在 69 行处添加 components注册该组件(data()上方):

js
components:{PageInfo },

1

大概在 61 行处,添加如下内容:

js
<PageInfostyle="margin-left:0"/>

1

效果图:

image-20241226124353587

核心配置文件

在 docs/.vuepress 目录下创建 webSiteInfo 文件夹,并在文件夹里创建 readFile.js文件。

添加如下内容:

js

js
constfs=require('fs');constpath=require('path');constmatter=require('gray-matter');constchalk=require('chalk') constlog=console.logconstdocsRoot=path.join(__dirname,'..','..','..','docs');functionreadFileList(excludeFiles=[''],dir=docsRoot,filesList=[]) {constfiles=fs.readdirSync(dir);files.forEach((item,index) =>{letfilePath =path.join(dir,item);conststat=fs.statSync(filePath);if(!(excludeFiles instanceofArray)) {log(chalk.yellow(`error: 传入的参数不是一个数组。`))}excludeFiles.forEach((excludeFile) =>{if(stat.isDirectory() &&item !=='.vuepress'&&item !=='@pages'&&item !==excludeFile) {readFileList(excludeFiles,path.join(dir,item),filesList);} else{if(path.basename(dir) !=='docs') {constfileNameArr=path.basename(filePath).split('.')letname =null,type =null;if(fileNameArr.length===2) {name =fileNameArr[0]type =fileNameArr[1]} elseif(fileNameArr.length===3) {name =fileNameArr[1]type =fileNameArr[2]} else{log(chalk.yellow(`warning: 该文件 "${filePath}" 没有按照约定命名,将忽略生成相应数据。`))return}if(type ==='md') {filesList.push({name,filePath});}}}});});returnfilesList;}functionreadTotalFileWords(excludeFiles=['']) {constfilesList=readFileList(excludeFiles);varwordCount =0;filesList.forEach((item) =>{constcontent=getContent(item.filePath);varlen =counter(content);wordCount +=len[0] +len[1];});if(wordCount <1000) {returnwordCount;}returnMath.round(wordCount /100) /10+'k';}functionreadEachFileWords(excludeFiles=[''],cn,en) {constfilesListWords=[];constfilesList=readFileList(excludeFiles);filesList.forEach((item) =>{constcontent=getContent(item.filePath);varlen =counter(content);varreadingTime =readTime(len,cn,en);varwordsCount =0;wordsCount =len[0] +len[1];if(wordsCount >=1000) {wordsCount =Math.round(wordsCount /100) /10+'k';}constfileMatterObj=matter(content,{});constmatterData=fileMatterObj.data;filesListWords.push({...item,wordsCount,readingTime,...matterData });});returnfilesListWords;}functionreadTime(len,cn=300,en=160) {varreadingTime =len[0] /cn +len[1] /en;if(readingTime >60&&readingTime <60*24) {lethour =parseInt(readingTime /60);letminute =parseInt((readingTime -hour *60));if(minute ===0) {returnhour +'h';}returnhour +'h'+minute +'m';} elseif(readingTime >60*24) {letday =parseInt(readingTime /(60*24));lethour =parseInt((readingTime -day *24*60) /60);if(hour ===0) {returnday +'d';}returnday +'d'+hour +'h';}returnreadingTime <1?'1':parseInt((readingTime *10)) /10+'m';}functiongetContent(filePath) {returnfs.readFileSync(filePath,'utf8');}functioncounter(content) {constcn=(content.match(/[\u4E00-\u9FA5]/g) ||[]).length;consten=(content.replace(/[\u4E00-\u9FA5]/g,'').match(/[a-zA-Z0-9_\u0392-\u03c9\u0400-\u04FF]+|[\u4E00-\u9FFF\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af\u0400-\u04FF]+|[\u00E4\u00C4\u00E5\u00C5\u00F6\u00D6]+|\w+/g) ||[]).length;return[cn,en];}module.exports={readFileList,readTotalFileWords,readEachFileWords,}

配置站点信息

最后一步,在 docs/.vuepress/config.js(新版为 config.ts)文件,引入写好的 readFile.js文件(路径要准确,这里仅仅是模板)

js
const{readFileList,readTotalFileWords,readEachFileWords} =require('./webSiteInfo/readFile');

1

如图(演示 JS 代码块):

image-20241226124432334

在 themeConfig 中添加如下内容:

js
blogInfo:{blogCreate:'2021-10-19',indexView:true,pageView:true,readingTime:true,eachFileWords:readEachFileWords([''],300,160),mdFileCountType:'archives',totalWords:'archives',moutedEvent:'.tags-wrapper',indexIteration:2500,pageIteration:2500,},

如图(图片内容不一定是最新):

image-20241226124456436

属性配置的具体介绍请看 属性配置

属性配置

blogCreate

  • 类型:string
  • 默认值:当前时间(new Date()
  • 格式:yyyy-mm-dd

博客创建时间。如果不添加时间,页面上显示 0 天。

mdFileCountType

  • 类型:string|readFileList()
  • 参数:数组
  • 默认值:archives

文章数目。如果不添加内容,页面上显示归档的文章数目。

readFileList是一个 js 文件,需要引入,参数是 目录的全名,最终效果会 排除该目录里的文章数,可多选,逗号隔开。也可不传参数。

温馨提示:readFileList()不传参数会获取 docs 下所有的 md 文档(除了 .vuepress@pages目录下的文档)。

totalWords

  • 类型:string|readFileWords()
  • 参数:数组
  • 默认值:null

本站文档总字数。如果不添加内容,页面上显示 0 字。

string仅支持 archives,并且使用该类型有条件:必须使用 eachFileWords,否则报错。

readFileWords是一个 js 文件,需要引入,参数是目录的全名,最终效果会 排除该目录里的文章字数,可多选,逗号隔开。也可不传参数。

moutedEvent

  • 类型:string
  • 默认值:.tags-wrapper

选择挂载的元素属性,支持多种选择器(id、class ......),该模块会挂载到该元素后面,形成兄弟元素。(仅支持首页的元素)。

温馨提示:.categories-wrapper会挂载在文章分类下面;.blogger-wrapper会挂载在头像模块下面;.icons会挂载在头像下方、图标上方。

默认是热门标签 .tags-wrapper下面。

indexView

  • 类型:boolean
  • 默认值:true

开启首页的访问量和排名统计,默认 true(开启)。

pageView

  • 类型 boolean
  • 默认值:true

开启文章页的浏览量统计,默认 true(开启)。

eachFileWords

  • 类型:readEachFileWords()
  • 参数:数组
  • 默认值:null

开启每个文章页的字数。如果不添加内容,则不开启。

readEachFileWords(['xx'])关闭 xx 目录(可多个,可不传参数)下的文章页字数和阅读时长。

readEachFileWords()第一个参数是数组,后面两个参数分别是 1 分钟里能阅读的中文字数和英文字数,配合 readingTime使用。

readEachFileWords()方法默认排除了 article 为 false 的文章。

readingTime

  • 类型:boolean
  • 默认值:true
  • 条件:使用 eachFileWords

开启文章页的预计阅读时间。默认阅读中文 1 分钟 300 个字,英文 1 分钟 160 个字。如果想自定义阅读文字时长,请在 eachFileWordsreadEachFileWords()传入后面两个参数。分别为 1 分钟阅读的中文和英文个数。

indexIteration

  • 类型:number
  • 默认值:3000

如果首页获取访问量失败,则每隔多少时间后获取一次访问量,直到获取成功或获取 10 次后。默认 3 秒。

注意:设置时间太低,可能导致访问量 + 2、+ 3 ......

pageIteration

  • 类型:number
  • 默认值:3000

如果文章页获取访问量失败,则每隔多少时间后获取一次访问量,直到获取成功或获取 10 次后。默认 3 秒。

注意:设置时间太低,可能导致访问量 + 2、+ 3 ......

说明:成功获取一次访问量,访问量 + 1,所以第一次获取失败后,设置的每个隔段重新获取时间,将会影响访问量的次数。如 100 可能每次获取访问量 + 3。

结束语

如果你还有疑惑,可以去我的 GitHub 仓库或者 Gitee 仓库查看源码。

如果你有更好的方式,评论区留言告诉我,或者加入 Vdoing 主题的 QQ 群:694387113。谢谢!

自己配置完的效果

image-20241226124533268

其它方案-自建busuanzi

2024年12月26日记录

image-20241226124734140

image-20241226124748139