本站 - 站点信息模块原创
笔记
本内容介绍如何搭建本站首页的站点信息,以及每篇文章的浏览量统计。
2021-12-30 @Young Kbt
# 前言
本内容将在首页和每篇的文章页加入了一些元素,目前适用版本是 Vdoing v1.x。
如果你想集成到其他 Vuepress 主题,那么要添加卡片样式,修改挂载元素即可(建议先按照步骤完成一次再考虑集成)。
- 为什么添加卡片样式?本模块的站点信息是基于 Vdoing 自带的卡片样式,模块并没有添加任何卡片样式,所以想集成到其他主题,则需要参考 Vdoing 卡片样式进行添加,或者按照自己喜欢的样式进行添加
- 为什么修改挂载元素?本模块的挂载元素是基于 Vdoing 标签提供的 class 或 id,而其他主题的标签不一样,所以自行进行调试
本模块的所有 功能 支持大部分 Vuepress 主题,但是如何将所有功能展示到其他主题页面合适的地方,以及展示的样式等 DOM 技术,需要自己适配。
效果如下:
本站的访问量和文章的浏览量使用了 不蒜子,本地启动的 localhost 有很多人访问过,但无需担心实际部署后的访问量。
注意
问题:本模块目前有一个功能依赖于 git 的 lastUpdated
功能,该功能已经内置 Vuepress,所以无需担心,唯一值得注意的是:在本地添加了新的文件,最后活动时间的数据可能为 NaN
(无法获取的意思)。
解决:只需要在博客项目部署的过程中执行 git commit
命令,因为该命令将会获取一个准确的时间代替 NaN
,给本模块使用。
2022-01-17 @Young Kbt
# 添加meta
为什么添加 meta 头信息呢,因为在 Chrome 85 版本中,为了保护用户的隐私,默认的 Referrer Policy 则变成了 strict-origin-when-cross-origin
。
所以必须添加 meta,否则文章统计访问量的数据则不正确。
在 docs/.vuepress/config.js 下的 head 中添加如下内容:
['meta', { name: 'referrer', content: 'no-referrer-when-downgrade' }],
如图:
# 添加在线图标
这里使用的是阿里矢量库。
地址:https://www.iconfont.cn/ (opens new window)
添加了五个图标
如果你不想使用自己的矢量库项目(不害怕我删图标跑路🤣),那么可以使用我的图标项目网址,当你发现图标失效了,就请来这里获取新的地址,如果还没有更新,请在评论区留言。
在 config.js 下的 head 中文件添加如下内容:
['link', { rel: 'stylesheet', href: 'https://at.alicdn.com/t/font_3077305_pt8umhrn4k9.css' }]
如图:(图片的内容不一定是最新的,以上方代码块为准)
# Vue模板
这里先提供一个在 Vue 里常用的模板代码,即通用代码(了解即可):
<template>
<div class="busuanzi">
<span id="busuanzi_container_site_pv" style="display:none">
本站总访问量
<span id="busuanzi_value_site_pv"></span>次
<span class="post-meta-divider">|</span>
</span>
<span id="busuanzi_container_site_uv" style="display:none">
本站访客数
<span id="busuanzi_value_site_uv"></span>人
</span>
</div>
</template>
<script>
let script;
export default {
mounted() {
script = require("busuanzi.pure.js");
},
// 监听,当路由发生变化的时候执行
watch: {
$route(to, from) {
if (to.path != from.path) {
script.fetch();
}
}
}
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 主题选择
下面有两种配置方式可以选,分别为:
- 在线主题:NPM 主题,采用监听路由、插入式的代码
- 本地主题:站点信息模块与页面一起渲染出来,没有延迟
本地主题不好的一点就是版本升级后曾修改的内容被重置,所以需要记好修改位置、备份,比较麻烦。好处是根据自己的需求在基础上拓展。
在线主题具有通用性,即在任意环境(如本地主题)都有效果。
# 在线主题
建议:本内容代码块比较长,可以点击代码块的右侧箭头来折叠,然后点击复制图标进行复制即可。
不管使不使用本地主题,都可以配置在线主题的站点模块。
# 网站信息工具代码
添加网站信息需要的计算代码、获取字数代码等工具类。
首先进入 docs/.vuepress 目录,创建 webSiteInfo
文件夹
然后在 webSiteInfo 目录下创建 busuanzi.js
文件,这个文件用于 获取访问量。
var bszCaller, bszTag, scriptTag, ready;
var t,
e,
n,
a = !1,
c = [];
// 修复Node同构代码的问题
if (typeof document !== "undefined") {
(ready = function (t) {
return (
a ||
"interactive" === document.readyState ||
"complete" === document.readyState
? t.call(document)
: c.push(function () {
return t.call(this);
}),
this
);
}),
(e = function () {
for (var t = 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) {
var n = "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) {
return function (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) {
var e = document.getElementById("busuanzi_value_" + t);
e && (e.innerHTML = n[t]);
});
},
hides: function () {
this.bszs.map(function (t) {
var e = document.getElementById("busuanzi_container_" + t);
e && (e.style.display = "none");
});
},
shows: function () {
this.bszs.map(function (t) {
var e = document.getElementById("busuanzi_container_" + t);
e && (e.style.display = "inline");
});
},
};
export default () => {
bszTag && bszTag.hides();
bszCaller.fetch("//busuanzi.ibruce.info/busuanzi?jsonpCallback=BusuanziCallback", function (t) {
bszTag.texts(t), bszTag.shows();
})
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
然后创建 readFile.js
或者 readFile.ts
文件,这个文件用于 统计文章数目 和 网站总字数 等。
添加如下内容:
接着继续在该目录下创建第三个文件 utils.js
,该文件用于计算 已运行时间 和 最后活动时间。
添加如下内容:
// 日期格式化(只获取年月日)
export function dateFormat(date) {
if (!(date instanceof Date)) {
date = new Date(date);
}
return `${date.getUTCFullYear()}-${zero(date.getUTCMonth() + 1)}-${zero(date.getUTCDate())}`;
}
// 小于10补0
export function zero(d) {
return d.toString().padStart(2, '0');
}
/**
* 计算最后活动时间
*/
export function lastUpdatePosts(posts) {
posts.sort((prev, next) => {
return compareDate(prev, next);
});
return posts;
}
// 获取时间的时间戳
export function getTimeNum(post) {
let dateStr = post.lastUpdated || post.frontmatter.date;
let date = new Date(dateStr);
if (date == "Invalid Date" && dateStr) { // 修复new Date()在Safari下出现Invalid Date的问题
date = new Date(dateStr.replace(/-/g, '/'));
}
return date.getTime();
}
// 比对时间
export function compareDate(a, b) {
return getTimeNum(b) - getTimeNum(a);
}
/**
* 获取两个日期相差多少天
*/
export function dayDiff(startDate, endDate) {
if (!endDate) {
endDate = startDate;
startDate = new Date();
}
startDate = dateFormat(startDate);
endDate = dateFormat(endDate);
let day = parseInt(Math.abs(new Date(startDate) - new Date(endDate)) / (1000 * 60 * 60 * 24));
return day;
}
/**
* 计算相差多少年/月/日/时/分/秒
*/
export function timeDiff(startDate, endDate) {
if (!endDate) {
endDate = startDate;
startDate = new Date();
}
if (!(startDate instanceof Date)) {
startDate = new Date(startDate);
}
if (!(endDate instanceof Date)) {
endDate = new Date(endDate);
}
// 计算时间戳的差
const diffValue = parseInt((Math.abs(endDate - startDate) / 1000));
if (diffValue == 0) {
return '刚刚';
} else if (diffValue < 60) {
return diffValue + ' 秒';
} else if (parseInt(diffValue / 60) < 60) {
return parseInt(diffValue / 60) + ' 分';
} else if (parseInt(diffValue / (60 * 60)) < 24) {
return parseInt(diffValue / (60 * 60)) + ' 时';
} else if (parseInt(diffValue / (60 * 60 * 24)) < getDays(startDate.getMonth, startDate.getFullYear)) {
return parseInt(diffValue / (60 * 60 * 24)) + ' 天';
} else if (parseInt(diffValue / (60 * 60 * 24 * getDays(startDate.getMonth, startDate.getFullYear))) < 12) {
return parseInt(diffValue / (60 * 60 * 24 * getDays(startDate.getMonth, startDate.getFullYear))) + ' 月';
} else {
return parseInt(diffValue / (60 * 60 * 24 * getDays(startDate.getMonth, startDate.getFullYear) * 12)) + ' 年';
}
}
/**
* 判断当前月的天数(28、29、30、31)
*/
export function getDays(mouth, year) {
let days = 30;
if (mouth === 2) {
days = year % 4 === 0 ? 29 : 28;
} else if (mouth === 1 || mouth === 3 || mouth === 5 || mouth === 7 || mouth === 8 || mouth === 10 || mouth === 12) {
// 月份为:1,3,5,7,8,10,12 时,为大月.则天数为 31;
days = 31;
}
return days;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
目前就三个文件,最终效果如图:
# 站点信息代码
这一步的文件目录不能随便移动,因为该目录是 Vuepress 规定的。
首先进入 docs/.vuepress 目录,创建 components 文件夹
创建一个 vue 文件:WebInfo.vue
,这就是首页的站点信息模块。
并添加如下内容:
<template>
<!-- Young Kbt -->
<div class="web-info card-box">
<div class="webinfo-title">
<i
class="iconfont icon-award"
style="font-size: 0.875rem; font-weight: 900; width: 1.25em"
></i>
<span>站点信息</span>
</div>
<div class="webinfo-item">
<div class="webinfo-item-title">文章数目:</div>
<div class="webinfo-content">{{ mdFileCount }} 篇</div>
</div>
<div class="webinfo-item">
<div class="webinfo-item-title">已运行时间:</div>
<div class="webinfo-content">
{{ createToNowDay != 0 ? createToNowDay + " 天" : "不到一天" }}
</div>
</div>
<div class="webinfo-item">
<div class="webinfo-item-title">本站总字数:</div>
<div class="webinfo-content">{{ totalWords }} 字</div>
</div>
<div class="webinfo-item">
<div class="webinfo-item-title">最后活动时间:</div>
<div class="webinfo-content">
{{ lastActiveDate == "刚刚" ? "刚刚" : lastActiveDate + "前" }}
</div>
</div>
<div v-if="indexView" class="webinfo-item">
<div class="webinfo-item-title">本站被访问了:</div>
<div class="webinfo-content">
<span id="busuanzi_value_site_pv" class="web-site-pv"
><i title="正在获取..." class="loading iconfont icon-loading"></i>
</span>
次
</div>
</div>
<div v-if="indexView" class="webinfo-item">
<div class="webinfo-item-title">您的访问排名:</div>
<div class="webinfo-content busuanzi">
<span id="busuanzi_value_site_uv" class="web-site-uv"
><i title="正在获取..." class="loading iconfont icon-loading"></i>
</span>
名
</div>
</div>
</div>
</template>
<script>
import { dayDiff, timeDiff, lastUpdatePosts } from "../webSiteInfo/utils";
import fetch from "../webSiteInfo/busuanzi"; // 统计量
export default {
data() {
return {
// Young Kbt
mdFileCount: 0, // markdown 文档总数
createToNowDay: 0, // 博客创建时间距今多少天
lastActiveDate: "", // 最后活动时间
totalWords: 0, // 本站总字数
indexView: true, // 开启访问量和排名统计
};
},
computed: {
$lastUpdatePosts() {
return lastUpdatePosts(this.$filterPosts);
},
},
mounted() {
// Young Kbt
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) {
let archivesWords = 0;
eachFileWords.forEach((itemFile) => {
if (itemFile.wordsCount < 1000) {
archivesWords += itemFile.wordsCount;
} else {
let wordsCount = itemFile.wordsCount.slice(
0,
itemFile.wordsCount.length - 1
);
archivesWords += wordsCount * 1000;
}
});
this.totalWords = Math.round(archivesWords / 100) / 10 + "k";
} else if (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") {
let interval = setInterval(() => {
const tagsWrapper = document.querySelector(moutedEvent);
const webInfo = document.querySelector(".web-info");
if (tagsWrapper && webInfo) {
if (!this.isSiblilngNode(tagsWrapper, webInfo)) {
tagsWrapper.parentNode.insertBefore(
webInfo,
tagsWrapper.nextSibling
);
clearInterval(interval);
}
}
}, 200);
},
/**
* 挂载在兄弟元素后面,说明当前组件是 siblingNode 变量
*/
isSiblilngNode(element, siblingNode) {
if (element.siblingNode == siblingNode) {
return true;
} else {
return false;
}
},
/**
* 首页的统计量
*/
getIndexViewCouter(iterationTime = 3000) {
fetch();
var i = 0;
var defaultCouter = "9999";
// 如果只需要第一次获取数据(可能获取失败),可注释掉 setTimeout 内容,此内容是第一次获取失败后,重新获取访问量
// 可能会导致访问量再次 + 1 原因:取决于 setTimeout 的时间(需求调节),setTimeout 太快导致第一个获取的数据没返回,就第二次获取,导致结果返回 + 2 的数据
setTimeout(() => {
let indexUv = document.querySelector(".web-site-pv");
let indexPv = document.querySelector(".web-site-uv");
if (
indexPv &&
indexUv &&
indexPv.innerText == "" &&
indexUv.innerText == ""
) {
let interval = setInterval(() => {
// 再次判断原因:防止进入 setInterval 的瞬间,访问量获取成功
if (
indexPv &&
indexUv &&
indexPv.innerText == "" &&
indexUv.innerText == ""
) {
i += iterationTime;
if (i > iterationTime * 5) {
indexPv.innerText = defaultCouter;
indexUv.innerText = defaultCouter;
clearInterval(interval); // 5 次后无法获取,则取消获取
}
if (indexPv.innerText == "" && indexUv.innerText == "") {
// 手动获取访问量
fetch();
} else {
clearInterval(interval);
}
} else {
clearInterval(interval);
}
}, iterationTime);
// 绑定 beforeDestroy 生命钩子,清除定时器
this.$once("hook:beforeDestroy", () => {
clearInterval(interval);
interval = null;
});
}
}, iterationTime);
},
beforeMount() {
let webInfo = document.querySelector(".web-info");
webInfo && webInfo.parentNode.removeChild(webInfo);
},
},
};
</script>
<style scoped>
.web-info {
font-size: 0.875rem;
padding: 0.95rem;
}
.webinfo-title {
text-align: center;
color: #888;
font-weight: bold;
padding: 0 0 10px 0;
}
.webinfo-item {
padding: 8px 0 0;
margin: 0;
}
.webinfo-item-title {
display: inline-block;
}
.webinfo-content {
display: inline-block;
float: right;
}
@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;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
继续创建一个 vue 文件:PageInfo.vue
,这就是文章页的信息模块:文章浏览量、字数代码、预阅读时间。
<template></template>
<script>
import fetch from "../webSiteInfo/busuanzi";
export default {
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) {
// 排除掉 article 为 false 的文章
const { eachFileWords, pageView, pageIteration, readingTime } =
this.$themeConfig.blogInfo;
// 下面两个 if 可以调换位置,从而让文章的浏览量和字数交换位置
if (eachFileWords) {
try {
eachFileWords.forEach((itemFile) => {
if (itemFile.permalink == this.$frontmatter.permalink) {
// this.addPageWordsCount 和 if 可以调换位置,从而让文章的字数和预阅读时间交换位置
this.addPageWordsCount(itemFile.wordsCount);
if (readingTime || readingTime == undefined) {
this.addReadTimeCount(itemFile.readingTime);
}
throw new Error();
}
});
} catch (error) {}
}
if (pageView || pageView == undefined) {
this.addPageView();
this.getPageViewCouter(pageIteration);
}
return;
}
},
/**
* 文章页的访问量
*/
getPageViewCouter(iterationTime = 3000) {
fetch();
let i = 0;
var defaultCouter = "9999";
// 如果只需要第一次获取数据(可能获取失败),可注释掉 setTimeout 内容,此内容是第一次获取失败后,重新获取访问量
// 可能会导致访问量再次 + 1 原因:取决于 setTimeout 的时间(需求调节),setTimeout 太快导致第一个获取的数据没返回,就第二次获取,导致结果返回 + 2 的数据
setTimeout(() => {
let pageView = document.querySelector(".view-data");
if (pageView && pageView.innerText == "") {
let interval = setInterval(() => {
// 再次判断原因:防止进入 setInterval 的瞬间,访问量获取成功
if (pageView && pageView.innerText == "") {
i += iterationTime;
if (i > iterationTime * 5) {
pageView.innerText = defaultCouter;
clearInterval(interval); // 5 次后无法获取,则取消获取
}
if (pageView.innerText == "") {
// 手动获取访问量
fetch();
} else {
clearInterval(interval);
}
} else {
clearInterval(interval);
}
}, iterationTime);
// 绑定 beforeDestroy 生命钩子,清除定时器
this.$once("hook:beforeDestroy", () => {
clearInterval(interval);
interval = null;
});
}
}, iterationTime);
},
/**
* 添加浏览量元素
*/
addPageView() {
let pageView = 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 {
// 创建访问量的元素
let template = 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>';
// 添加 loading 效果
let style = 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) {
let words = document.querySelector(".book-words");
if (words) {
words.innerHTML = `<a href="javascript:;" style="margin-left: 3px; color: #888">${wordsCount}</a>`;
} else {
let template = 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) {
let reading = document.querySelector(".reading-time");
if (reading) {
reading.innerHTML = `<a href="javascript:;" style="margin-left: 3px; color: #888">${readTimeCount}</a>`;
} else {
let template = 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"
) {
let i = 0;
let parentElement = document.querySelector(moutedParentEvent);
if (parentElement) {
if (!this.isMountedView(template, parentElement)) {
parentElement.appendChild(template);
}
} else {
let interval = setInterval(() => {
parentElement = document.querySelector(moutedParentEvent);
if (parentElement) {
if (!this.isMountedView(template, parentElement)) {
parentElement.appendChild(template);
clearInterval(interval);
}
} else if (i > 1 * 10) {
// 10 秒后清除
clearInterval(interval);
}
}, mountedIntervalTime);
// 绑定 beforeDestroy 生命钩子,清除定时器
this.$once("hook:beforeDestroy", () => {
clearInterval(interval);
interval = null;
});
}
},
/**
* 如果元素存在,则删除
*/
removeElement(selector) {
var element = document.querySelector(selector);
element && element.parentNode.removeChild(element);
},
/**
* 目标是否已经挂载在页面上
*/
isMountedView(element, parentElement) {
if (element.parentNode == parentElement) {
return true;
} else {
return false;
}
},
},
// 防止重写编译时,导致页面信息重复出现问题
beforeMount() {
clearInterval(this.interval);
this.removeElement(".page-view");
this.removeElement(".book-words");
this.removeElement(".reading-time");
},
};
</script>
<style></style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
最终效果如图:
创建好了两个 vue 组件,我们需要使用它们。
使用
WebInfo.vue
组件
打开 docs/index.md
移到最下方,添加如下内容:
<ClientOnly>
<WebInfo/>
</ClientOnly>
2
3
使用
PageInfo.vue
组件
在 docs/.vuepress/config.js(新版是 config.ts)的 plugins 中添加配置。
# 站点信息配置
上面都按照步骤写好代码、使用组件了,那么就可以走最后一步配置我们的站点信息。
进入到 docs/.vuepress/config.js(新版为 config.ts)文件。
引入之前写好的工具代码文件:(路径要准确,这里仅仅是模板)
如图(演示 JS 代码块):
在 themeConfig 中添加如下内容:
// 站点配置(首页 & 文章页)
blogInfo: {
blogCreate: '2021-10-19', // 博客创建时间
indexView: true, // 开启首页的访问量和排名统计,默认 true(开启)
pageView: true, // 开启文章页的浏览量统计,默认 true(开启)
readingTime: true, // 开启文章页的预计阅读时间,条件:开启 eachFileWords,默认 true(开启)。可在 eachFileWords 的 readEachFileWords 的第二个和第三个参数自定义,默认 1 分钟 300 中文、160 英文
eachFileWords: readEachFileWords([''], 300, 160), // 开启每个文章页的字数。readEachFileWords(['xx']) 关闭 xx 目录(可多个,可不传参数)下的文章页字数和阅读时长,后面两个参数分别是 1 分钟里能阅读的中文字数和英文字数。无默认值。readEachFileWords() 方法默认排除了 article 为 false 的文章
mdFileCountType: 'archives', // 开启文档数。1. archives 获取归档的文档数(默认)。2. 数组 readFileList(['xx']) 排除 xx 目录(可多个,可不传参数),获取其他目录的文档数。提示:readFileList() 获取 docs 下所有的 md 文档(除了 `.vuepress` 和 `@pages` 目录下的文档)
totalWords: 'archives', // 开启本站文档总字数。1. archives 获取归档的文档数(使用 archives 条件:传入 eachFileWords,否则报错)。2. readTotalFileWords(['xx']) 排除 xx 目录(可多个,可不传参数),获取其他目录的文章字数。无默认值
moutedEvent: '.tags-wrapper', // 首页的站点模块挂载在某个元素后面(支持多种选择器),指的是挂载在哪个兄弟元素的后面,默认是热门标签 '.tags-wrapper' 下面,提示:'.categories-wrapper' 会挂载在文章分类下面。'.blogger-wrapper' 会挂载在博客头像模块下面
// 下面两个选项:第一次获取访问量失败后的迭代时间
indexIteration: 2500, // 如果首页获取访问量失败,则每隔多少时间后获取一次访问量,直到获取成功或获取 10 次后。默认 3 秒。注意:设置时间太低,可能导致访问量 + 2、+ 3 ......
pageIteration: 2500, // 如果文章页获取访问量失败,则每隔多少时间后获取一次访问量,直到获取成功或获取 10 次后。默认 3 秒。注意:设置时间太低,可能导致访问量 + 2、+ 3 ......
// 说明:成功获取一次访问量,访问量 + 1,所以第一次获取失败后,设置的每个隔段重新获取时间,将会影响访问量的次数。如 100 可能每次获取访问量 + 3
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如图(图片内容不一定是最新,最新的是代码块内容):
属性配置的具体介绍请看 属性配置。
# 本地主题
如果已经看完了在线主题的内容,其实本内容的大小不变,只是位置变换、一些代码重组。
配置了在线主题,就不需要配置本地主题,反之亦然。
# 工具类
在 vdoing/util 目录下创建 webSiteInfo.js
,添加如下内容:
// 日期格式化(只获取年月日)
export function dateFormat(date) {
if (!(date instanceof Date)) {
date = new Date(date);
}
return `${date.getUTCFullYear()}-${zero(date.getUTCMonth() + 1)}-${zero(date.getUTCDate())}`;
}
// 小于10补0
export function zero(d) {
return d.toString().padStart(2, '0');
}
/**
* 计算最后活动时间
*/
export function lastUpdatePosts(posts) {
posts.sort((prev, next) => {
return compareDate(prev, next);
});
return posts;
}
// 获取时间的时间戳
export function getTimeNum(post) {
let dateStr = post.lastUpdated || post.frontmatter.date;
let date = new Date(dateStr);
if (date == "Invalid Date" && dateStr) { // 修复new Date()在Safari下出现Invalid Date的问题
date = new Date(dateStr.replace(/-/g, '/'));
}
return date.getTime();
}
// 比对时间
export function compareDate(a, b) {
return getTimeNum(b) - getTimeNum(a);
}
/**
* 获取两个日期相差多少天
*/
export function dayDiff(startDate, endDate) {
if (!endDate) {
endDate = startDate;
startDate = new Date();
}
startDate = dateFormat(startDate);
endDate = dateFormat(endDate);
let day = parseInt(Math.abs(new Date(startDate) - new Date(endDate)) / (1000 * 60 * 60 * 24));
return day;
}
/**
* 计算相差多少年/月/日/时/分/秒
*/
export function timeDiff(startDate, endDate) {
if (!endDate) {
endDate = startDate;
startDate = new Date();
}
if (!(startDate instanceof Date)) {
startDate = new Date(startDate);
}
if (!(endDate instanceof Date)) {
endDate = new Date(endDate);
}
// 计算时间戳的差
const diffValue = parseInt((Math.abs(endDate - startDate) / 1000));
if (diffValue == 0) {
return '刚刚';
} else if (diffValue < 60) {
return diffValue + ' 秒';
} else if (parseInt(diffValue / 60) < 60) {
return parseInt(diffValue / 60) + ' 分';
} else if (parseInt(diffValue / (60 * 60)) < 24) {
return parseInt(diffValue / (60 * 60)) + ' 时';
} else if (parseInt(diffValue / (60 * 60 * 24)) < getDays(startDate.getMonth, startDate.getFullYear)) {
return parseInt(diffValue / (60 * 60 * 24)) + ' 天';
} else if (parseInt(diffValue / (60 * 60 * 24 * getDays(startDate.getMonth, startDate.getFullYear))) < 12) {
return parseInt(diffValue / (60 * 60 * 24 * getDays(startDate.getMonth, startDate.getFullYear))) + ' 月';
} else {
return parseInt(diffValue / (60 * 60 * 24 * getDays(startDate.getMonth, startDate.getFullYear) * 12)) + ' 年';
}
}
/**
* 判断当前月的天数(28、29、30、31)
*/
export function getDays(mouth, year) {
let days = 30;
if (mouth === 2) {
days = year % 4 === 0 ? 29 : 28;
} else if (mouth === 1 || mouth === 3 || mouth === 5 || mouth === 7 || mouth === 8 || mouth === 10 || mouth === 12) {
// 月份为:1,3,5,7,8,10,12 时,为大月.则天数为 31;
days = 31;
}
return days;
}
/**
* 已运行时间低于一天显示时分秒
* 目前该函数没有使用,低于一天直接显示不到一天
*/
export function getTime(startDate, endDate) {
if (day < 0) {
let hour = parseInt(Math.abs(new Date(startDate) - new Date(endDate)) / (1000 * 60 * 60));
if (hour > 0) {
let minute = parseInt(Math.abs(new Date(startDate) - new Date(endDate) - hour * 60 * 60 * 1000) / (1000 * 60));
if (minute > 0) {
let second = parseInt(Math.abs(new Date(startDate) - new Date(endDate) - hour * 60 * 60 * 1000 - minute * 60 * 1000) / (1000));
if (second != 0) {
return hour + ' 小时 ' + minute + ' 分钟 ' + second + ' 秒';
} else {
return hour + ' 小时 ' + minute + ' 分钟 ';
}
} else {
return hour + ' 小时 ';
}
} else {
let minute = parseInt(Math.abs(new Date(startDate) - new Date(endDate) - hour * 60 * 60 * 1000) / (1000 * 60));
if (minute > 0) {
let second = parseInt(Math.abs(new Date(startDate) - new Date(endDate) - hour * 60 * 60 * 1000 - minute * 60 * 1000) / (1000));
if (second != 0) {
return + minute + ' 分钟 ' + second + ' 秒';
} else {
return minute + ' 分钟 ';
}
} else {
return parseInt(Math.abs(new Date(startDate) - new Date(endDate) - hour * 60 * 60 * 1000 - minute * 60 * 1000) / (1000)) + ' 秒 ';
}
}
}
}
var bszCaller, bszTag, scriptTag, ready;
var t,
e,
n,
a = !1,
c = [];
// 修复Node同构代码的问题
if (typeof document !== "undefined") {
(ready = function (t) {
return (
a ||
"interactive" === document.readyState ||
"complete" === document.readyState
? t.call(document)
: c.push(function () {
return t.call(this);
}),
this
);
}),
(e = function () {
for (var t = 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) {
var n = "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) {
return function (t) {
ready(function () {
try {
e(t),
scriptTag &&
scriptTag.parentElement &&
scriptTag.parentElement.removeChild &&
scriptTag.parentElement.removeChild(scriptTag);
} catch (t) {
console.log(t), bszTag.hides();
}
});
};
},
};
export function fetch() {
bszTag && bszTag.hides();
bszCaller.fetch("//busuanzi.ibruce.info/busuanzi?jsonpCallback=BusuanziCallback", function (t) {
bszTag.texts(t), bszTag.shows();
})
};
bszTag = {
bszs: ["site_pv", "page_pv", "site_uv"],
texts: function (n) {
this.bszs.map(function (t) {
var e = document.getElementById("busuanzi_value_" + t);
e && (e.innerHTML = n[t]);
});
},
hides: function () {
this.bszs.map(function (t) {
var e = document.getElementById("busuanzi_container_" + t);
e && (e.style.display = "none");
});
},
shows: function () {
this.bszs.map(function (t) {
var e = document.getElementById("busuanzi_container_" + t);
e && (e.style.display = "inline");
});
},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# Vue组件创建
需要两个 Vue 组件,分别是首页的站点信息模块和文章页信息模块。
在 vdoing/components 目录下创建 WebInfo.vue
文件,添加如下内容:
<template>
<!-- Young Kbt -->
<div class="web-info card-box">
<div class="webinfo-title">
<i
class="iconfont icon-award"
style="font-size: 0.875rem; font-weight: 900; width: 1.25em"
></i>
<span>站点信息</span>
</div>
<div class="webinfo-item">
<div class="webinfo-item-title">文章数目:</div>
<div class="webinfo-content">{{ mdFileCount }} 篇</div>
</div>
<div class="webinfo-item">
<div class="webinfo-item-title">已运行时间:</div>
<div class="webinfo-content">
{{ createToNowDay != 0 ? createToNowDay + " 天" : "不到一天" }}
</div>
</div>
<div class="webinfo-item">
<div class="webinfo-item-title">本站总字数:</div>
<div class="webinfo-content">{{ totalWords }} 字</div>
</div>
<div class="webinfo-item">
<div class="webinfo-item-title">最后活动时间:</div>
<div class="webinfo-content">
{{ lastActiveDate == "刚刚" ? "刚刚" : lastActiveDate + "前" }}
</div>
</div>
<div v-if="indexView" class="webinfo-item">
<div class="webinfo-item-title">本站被访问了:</div>
<div class="webinfo-content">
<span id="busuanzi_value_site_pv" class="web-site-pv"
><i title="正在获取..." class="loading iconfont icon-loading"></i>
</span>
次
</div>
</div>
<div v-if="indexView" class="webinfo-item">
<div class="webinfo-item-title">您的访问排名:</div>
<div class="webinfo-content busuanzi">
<span id="busuanzi_value_site_uv" class="web-site-uv"
><i title="正在获取..." class="loading iconfont icon-loading"></i>
</span>
名
</div>
</div>
</div>
</template>
<script>
import { dayDiff, timeDiff, lastUpdatePosts, fetch } from "../util/webSiteInfo";
export default {
data() {
return {
// Young Kbt
mdFileCount: 0, // markdown 文档总数
createToNowDay: 0, // 博客创建时间距今多少天
lastActiveDate: "", // 最后活动时间
totalWords: 0, // 本站总字数
indexView: true, // 开启访问量和排名统计
};
},
computed: {
$lastUpdatePosts() {
return lastUpdatePosts(this.$filterPosts);
},
},
mounted() {
// Young Kbt
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) {
let archivesWords = 0;
eachFileWords.forEach((itemFile) => {
if (itemFile.wordsCount < 1000) {
archivesWords += itemFile.wordsCount;
} else {
let wordsCount = itemFile.wordsCount.slice(
0,
itemFile.wordsCount.length - 1
);
archivesWords += wordsCount * 1000;
}
});
this.totalWords = Math.round(archivesWords / 100) / 10 + "k";
} else if (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") {
let interval = setInterval(() => {
const tagsWrapper = document.querySelector(moutedEvent);
const webInfo = document.querySelector(".web-info");
if (tagsWrapper && webInfo) {
if (!this.isSiblilngNode(tagsWrapper, webInfo)) {
tagsWrapper.parentNode.insertBefore(
webInfo,
tagsWrapper.nextSibling
);
clearInterval(interval);
}
}
}, 200);
},
/**
* 挂载在兄弟元素后面,说明当前组件是 siblingNode 变量
*/
isSiblilngNode(element, siblingNode) {
if (element.siblingNode == siblingNode) {
return true;
} else {
return false;
}
},
/**
* 首页的统计量
*/
getIndexViewCouter(iterationTime = 3000) {
fetch();
var i = 0;
var defaultCouter = "9999";
// 如果只需要第一次获取数据(可能获取失败),可注释掉 setTimeout 内容,此内容是第一次获取失败后,重新获取访问量
// 可能会导致访问量再次 + 1 原因:取决于 setTimeout 的时间(需求调节),setTimeout 太快导致第一个获取的数据没返回,就第二次获取,导致结果返回 + 2 的数据
setTimeout(() => {
let indexUv = document.querySelector(".web-site-pv");
let indexPv = document.querySelector(".web-site-uv");
if (
indexPv &&
indexUv &&
indexPv.innerText == "" &&
indexUv.innerText == ""
) {
let interval = setInterval(() => {
// 再次判断原因:防止进入 setInterval 的瞬间,访问量获取成功
if (
indexPv &&
indexUv &&
indexPv.innerText == "" &&
indexUv.innerText == ""
) {
i += iterationTime;
if (i > iterationTime * 5) {
indexPv.innerText = defaultCouter;
indexUv.innerText = defaultCouter;
clearInterval(interval); // 5 次后无法获取,则取消获取
}
if (indexPv.innerText == "" && indexUv.innerText == "") {
// 手动获取访问量
fetch();
} else {
clearInterval(interval);
}
} else {
clearInterval(interval);
}
}, iterationTime);
// 绑定 beforeDestroy 生命钩子,清除定时器
this.$once("hook:beforeDestroy", () => {
clearInterval(interval);
interval = null;
});
}
}, iterationTime);
},
},
};
</script>
<style scoped>
.web-info {
font-size: 0.875rem;
padding: 0.95rem;
}
.webinfo-title {
text-align: center;
color: #888;
font-weight: bold;
padding: 0 0 10px 0;
}
.webinfo-item {
padding: 8px 0 0;
margin: 0;
}
.webinfo-item-title {
display: inline-block;
}
.webinfo-content {
display: inline-block;
float: right;
}
@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;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
继续在 vdoing/components 目录下创建 PageInfo.vue
文件,添加如下内容:
<template>
<div class="page-view">
<!-- 文章字数 -->
<div title="文章字数" class="book-words iconfont icon-book">
<a href="javascript:;" style="margin-left: 3px; color: #888">{{
wordsCount
}}</a>
</div>
<!-- 预阅读时长 -->
<div
v-if="readingTime"
title="预阅读时长"
class="reading-time iconfont icon-shijian"
>
<a href="javascript:;" style="margin-left: 3px; color: #888">{{
readingTime
}}</a>
</div>
<!-- 浏览量 -->
<div v-if="pageView" title="浏览量" class="page-view iconfont icon-view">
<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>
</div>
</div>
</template>
<script>
import { fetch } from "../util/webSiteInfo";
export default {
data() {
return {
// Young Kbt
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();
let i = 0;
var defaultCouter = "9999";
// 如果只需要第一次获取数据(可能获取失败),可注释掉 setTimeout 内容,此内容是第一次获取失败后,重新获取访问量
// 可能会导致访问量再次 + 1 原因:取决于 setTimeout 的时间(需求调节),setTimeout 太快导致第一个获取的数据没返回,就第二次获取,导致结果返回 + 2 的数据
setTimeout(() => {
let pageView = document.querySelector(".view-data");
if (pageView && pageView.innerText == "") {
let interval = setInterval(() => {
// 再次判断原因:防止进入 setInterval 的瞬间,访问量获取成功
if (pageView && pageView.innerText == "") {
i += iterationTime;
if (i > iterationTime * 5) {
pageView.innerText = defaultCouter;
clearInterval(interval); // 5 次后无法获取,则取消获取
}
if (pageView.innerText == "") {
// 手动获取访问量
fetch();
} else {
clearInterval(interval);
}
} else {
clearInterval(interval);
}
}, iterationTime);
// 绑定 beforeDestroy 生命钩子,清除定时器
this.$once("hook:beforeDestroy", () => {
clearInterval(interval);
interval = null;
});
}
}, iterationTime);
},
},
};
</script>
<style scoped>
.page-view > div {
float: left;
margin-left: 20px;
font-size: 0.8rem;
}
@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;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# Vue组件引用
写好两个组件,那么我们需要使用它们。
引入
WebInfo.vue
组件
打开 vdoing/components/Home.vue
文件。
大概在 174 行处引入 WebInfo.vue
组件:
import WebInfo from './WebInfo.vue';
大概在 242 行处找到 components
注册该组件:
components: { ......, WebInfo },
大概在 153 行处(div 的 class 为 custom-html-box
的上方),添加如下内容:
<webInfo />
三个效果图:
引入
PageInfo.vue
组件
打开 vdoing/components/ArticleInfo.vue
文件。
大概在 67 行处引入 PagesView.vue
组件:
import PageInfo from './PageInfo.vue';
大概在 69 行处添加 components
注册该组件(data()
上方):
components: { PageInfo },
大概在 61 行处,添加如下内容:
<PageInfo style="margin-left: 0" />
效果图:
# 核心配置文件
在 docs/.vuepress 目录下创建 webSiteInfo 文件夹,并在文件夹里创建 readFile.js
文件。
添加如下内容:
# 配置站点信息
最后一步,在 docs/.vuepress/config.js(新版为 config.ts)文件,引入写好的 readFile.js
文件(路径要准确,这里仅仅是模板)
如图(演示 JS 代码块):
在 themeConfig 中添加如下内容:
// 站点配置(首页 & 文章页)
blogInfo: {
blogCreate: '2021-10-19', // 博客创建时间
indexView: true, // 开启首页的访问量和排名统计,默认 true(开启)
pageView: true, // 开启文章页的浏览量统计,默认 true(开启)
readingTime: true, // 开启文章页的预计阅读时间,条件:开启 eachFileWords,默认 true(开启)。可在 eachFileWords 的 readEachFileWords 的第二个和第三个参数自定义,默认 1 分钟 300 中文、160 英文
eachFileWords: readEachFileWords([''], 300, 160), // 开启每个文章页的字数。readEachFileWords(['xx']) 关闭 xx 目录(可多个,可不传参数)下的文章页字数和阅读时长,后面两个参数分别是 1 分钟里能阅读的中文字数和英文字数。无默认值。readEachFileWords() 方法默认排除了 article 为 false 的文章
mdFileCountType: 'archives', // 开启文档数。1. archives 获取归档的文档数(默认)。2. 数组 readFileList(['xx']) 排除 xx 目录(可多个,可不传参数),获取其他目录的文档数。提示:readFileList() 获取 docs 下所有的 md 文档(除了 `.vuepress` 和 `@pages` 目录下的文档)
totalWords: 'archives', // 开启本站文档总字数。1. archives 获取归档的文档数(使用 archives 条件:传入 eachFileWords,否则报错)。2. readTotalFileWords(['xx']) 排除 xx 目录(可多个,可不传参数),获取其他目录的文章字数。无默认值
moutedEvent: '.tags-wrapper', // 首页的站点模块挂载在某个元素后面(支持多种选择器),指的是挂载在哪个兄弟元素的后面,默认是热门标签 '.tags-wrapper' 下面,提示:'.categories-wrapper' 会挂载在文章分类下面。'.blogger-wrapper' 会挂载在博客头像模块下面
// 下面两个选项:第一次获取访问量失败后的迭代时间
indexIteration: 2500, // 如果首页获取访问量失败,则每隔多少时间后获取一次访问量,直到获取成功或获取 10 次后。默认 3 秒。注意:设置时间太低,可能导致访问量 + 2、+ 3 ......
pageIteration: 2500, // 如果文章页获取访问量失败,则每隔多少时间后获取一次访问量,直到获取成功或获取 10 次后。默认 3 秒。注意:设置时间太低,可能导致访问量 + 2、+ 3 ......
// 说明:成功获取一次访问量,访问量 + 1,所以第一次获取失败后,设置的每个隔段重新获取时间,将会影响访问量的次数。如 100 可能每次获取访问量 + 3
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如图(图片内容不一定是最新):
属性配置的具体介绍请看 属性配置。
# 属性配置
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 个字。如果想自定义阅读文字时长,请在 eachFileWords
的 readEachFileWords()
传入后面两个参数。分别为 1 分钟阅读的中文和英文个数。
indexIteration
- 类型:
number
- 默认值:3000
如果首页获取访问量失败,则每隔多少时间后获取一次访问量,直到获取成功或获取 10 次后。默认 3 秒。
注意:设置时间太低,可能导致访问量 + 2、+ 3 ......
pageIteration
- 类型:
number
- 默认值:3000
如果文章页获取访问量失败,则每隔多少时间后获取一次访问量,直到获取成功或获取 10 次后。默认 3 秒。
注意:设置时间太低,可能导致访问量 + 2、+ 3 ......
说明:成功获取一次访问量,访问量 + 1,所以第一次获取失败后,设置的每个隔段重新获取时间,将会影响访问量的次数。如 100 可能每次获取访问量 + 3。
# 结束语
如果你还有疑惑,可以去我的 GitHub 仓库或者 Gitee 仓库查看源码。
如果你有更好的方式,评论区留言告诉我,或者加入 Vdoing 主题的 QQ 群:694387113。谢谢!