站点信息模块原创
说明
本内容介绍如何搭建本站首页的站点信息,以及每篇文章的浏览量统计,主要改动了文章页的浏览量统计,其余部分参考站点信息模块 (opens new window)
# 前言
本内容将在首页和每篇的文章页加入了一些元素,目前适用版本是 Vdoing v1.x。
如果你想集成到其他 Vuepress 主题,那么要添加卡片样式,修改挂载元素即可(建议先按照步骤完成一次再考虑集成)。
- 为什么添加卡片样式?本模块的站点信息是基于 Vdoing 自带的卡片样式,模块并没有添加任何卡片样式,所以想集成到其他主题,则需要参考 Vdoing 卡片样式进行添加,或者按照自己喜欢的样式进行添加
- 为什么修改挂载元素?本模块的挂载元素是基于 Vdoing 标签提供的 class 或 id,而其他主题的标签不一样,所以自行进行调试
本模块的所有 功能 支持大部分 Vuepress 主题,但是如何将所有功能展示到其他主题页面合适的地方,以及展示的样式等 DOM 技术,需要自己适配。
效果如下:
本站的访问量和文章的浏览量使用了 自建不蒜子
注意
问题:本模块目前有一个功能依赖于 git 的 lastUpdated
功能,该功能已经内置 Vuepress,所以无需担心,唯一值得注意的是:在本地添加了新的文件,最后活动时间的数据可能为 NaN
(无法获取的意思)。
解决:只需要在博客项目部署的过程中执行 git commit
命令,因为该命令将会获取一个准确的时间代替 NaN
,给本模块使用
# 添加head
为什么添加 meta 头信息呢,因为在 Chrome 85 版本中,为了保护用户的隐私,默认的 Referrer Policy 则变成了 strict-origin-when-cross-origin
。
所以必须添加 meta,否则文章统计访问量的数据则不正确。
在 docs/.vuepress/config.js 下的 head 中添加如下内容:
['meta', { name: 'referrer', content: 'no-referrer-when-downgrade' }],
['link', { rel: 'stylesheet', href: 'https://at.alicdn.com/t/font_3077305_pt8umhrn4k9.css' }],
['link', { rel: 'stylesheet', href: '//at.alicdn.com/t/c/font_4397361_l7w8pg1gfn.css' }],
2
3
# 网站信息工具代码
首先进入 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 = 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) {
bszTag.hides();
}
});
};
},
};
bszTag = {
bszs: ["site_pv", "site_uv", "page_pv", "page_uv"],
texts: function (n) {
this.bszs.map(function (t) {
var e = document.getElementById("busuanzi_" + 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 () => {
bszCaller.fetch("//busuanzi.9420.ltd/js", 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
文件,这个文件用于 统计文章数目 和 网站总字数 等。
const fs = require('fs'); // 文件模块
const path = require('path'); // 路径模块
const matter = require('gray-matter'); // FrontMatter解析器 https://github.com/jonschlinkert/gray-matter
const chalk = require('chalk') // 命令行打印美化
const log = console.log
const docsRoot = path.join(__dirname, '..', '..', '..', 'docs'); // docs文件路径
/**
* 获取本站的文章数据
* 获取所有的 md 文档,可以排除指定目录下的文档
*/
function readFileList(excludeFiles = [''], dir = docsRoot, filesList = []) {
const files = fs.readdirSync(dir);
files.forEach((item, index) => {
let filePath = path.join(dir, item);
const stat = fs.statSync(filePath);
if (!(excludeFiles instanceof Array)) {
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') { // 过滤 docs目录级下的文件
const fileNameArr = path.basename(filePath).split('.')
let name = null, type = null;
if (fileNameArr.length === 2) { // 没有序号的文件
name = fileNameArr[0]
type = fileNameArr[1]
} else if (fileNameArr.length === 3) { // 有序号的文件
name = fileNameArr[1]
type = fileNameArr[2]
} else { // 超过两个‘.’的
log(chalk.yellow(`warning: 该文件 "${filePath}" 没有按照约定命名,将忽略生成相应数据。`))
return
}
if (type === 'md') { // 过滤非 md 文件
filesList.push({
name,
filePath
});
}
}
}
});
});
return filesList;
}
/**
* 获取本站的文章总字数
* 可以排除某个目录下的 md 文档字数
*/
function readTotalFileWords(excludeFiles = ['']) {
const filesList = readFileList(excludeFiles);
var wordCount = 0;
filesList.forEach((item) => {
const content = getContent(item.filePath);
var len = counter(content);
wordCount += len[0] + len[1];
});
if (wordCount < 1000) {
return wordCount;
}
return Math.round(wordCount / 100) / 10 + 'k';
}
/**
* 获取每一个文章的字数
* 可以排除某个目录下的 md 文档字数
*/
function readEachFileWords(excludeFiles = [''], cn, en) {
const filesListWords = [];
const filesList = readFileList(excludeFiles);
filesList.forEach((item) => {
const content = getContent(item.filePath);
var len = counter(content);
// 计算预计的阅读时间
var readingTime = readTime(len, cn, en);
var wordsCount = 0;
wordsCount = len[0] + len[1];
if (wordsCount >= 1000) {
wordsCount = Math.round(wordsCount / 100) / 10 + 'k';
}
// fileMatterObj => {content:'剔除frontmatter后的文件内容字符串', data:{<frontmatter对象>}, ...}
const fileMatterObj = matter(content, {});
const matterData = fileMatterObj.data;
filesListWords.push({ ...item, wordsCount, readingTime, ...matterData });
});
return filesListWords;
}
/**
* 计算预计的阅读时间
*/
function readTime(len, cn = 300, en = 160) {
var readingTime = len[0] / cn + len[1] / en;
if (readingTime > 60 && readingTime < 60 * 24) { // 大于一个小时,小于一天
let hour = parseInt(readingTime / 60);
let minute = parseInt((readingTime - hour * 60));
if (minute === 0) {
return hour + 'h';
}
return hour + 'h' + minute + 'm';
} else if (readingTime > 60 * 24) { // 大于一天
let day = parseInt(readingTime / (60 * 24));
let hour = parseInt((readingTime - day * 24 * 60) / 60);
if (hour === 0) {
return day + 'd';
}
return day + 'd' + hour + 'h';
}
return readingTime < 1 ? '1' : parseInt((readingTime * 10)) / 10 + 'm'; // 取一位小数
}
/**
* 读取文件内容
*/
function getContent(filePath) {
return fs.readFileSync(filePath, 'utf8');
}
/**
* 获取文件内容的字数
* cn:中文
* en:一整句英文(没有空格隔开的英文为 1 个)
*/
function counter(content) {
const cn = (content.match(/[\u4E00-\u9FA5]/g) || []).length;
const en = (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,
}
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
# 接着继续在该目录下创建第三个文件 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
其次进入 docs/.vuepress 目录,创建 components 文件夹,这一步的文件目录不能随便移动,因为该目录是 Vuepress 规定的。
# 在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> -->
<span id="busuanzi_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> -->
<span id="busuanzi_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
249
250
251
252
253
254
255
256
257
258
# 然后创建一个 vue 文件:PageInfo.vue
,这就是文章页的信息模块
包含文章浏览量、字数代码、预阅读时间
<template>
<div class="page-info">
<!-- 当前文章页字数 -->
<div class="book-words iconfont icon-book" style="float: left; margin-left: 20px; font-size: 0.8rem;">
<a href="javascript:;" style="margin-left: 3px; color: #888">{{ wordsCount }}</a>
</div>
<!-- 预计阅读时间 -->
<div class="reading-time iconfont icon-shijian" style="float: left; margin-left: 20px; font-size: 0.8rem;">
<a href="javascript:;" style="margin-left: 3px; color: #888">{{ readTimeCount }}</a>
</div>
<!-- 文章页访问量 -->
<div class="page-view iconfont icon-view" style="float: left; margin-left: 20px; font-size: 0.8rem;">
<a href="javascript:;" id="busuanzi_page_pv" class="view-data">
<i title="正在获取..." class="loading iconfont icon-loading"></i>
</a>
</div>
<!-- 本文总访客量 -->
<div class="page_total_view iconfont icon-tongji" style="float: left; margin-left: 20px; font-size: 0.8rem;">
<a href="javascript:;" id="busuanzi_page_uv" class="view-data">
<i title="正在获取..." class="loading iconfont icon-loading"></i>
</a>
</div>
</div>
</template>
<script>
import fetch from "../webSiteInfo/busuanzi";
export default {
data() {
return {
wordsCount: 0,
readTimeCount: 0,
mountedIntervalTime: 1000,
// showPageInfo: true,
moutedParentEvent: ".articleInfo-wrap > .articleInfo > .info"
};
},
mounted: function () {
this.$nextTick(function () {
if (this.$route.path != "/") {
this.initPageInfo();
this.isMounted(document.querySelector(".page-info"));
}
})
},
watch: {
$route(to, from) {
// 如果页面是非首页,# 号也会触发路由变化,这里要排除掉
if (to.path != "/" && to.path != from.path && this.$themeConfig.blogInfo) {
this.initPageInfo();
this.isMounted(document.querySelector(".page-info"));
}
},
},
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.wordsCount = itemFile.wordsCount;
if (readingTime || readingTime === undefined) {
this.readTimeCount = itemFile.readingTime;
}
}
});
} catch (error) {
console.error("获取浏览量失败:", error);
}
}
if (pageView || pageView === undefined) {
this.addPageView();
this.addtotalPageView()
this.getPageViewCouter(pageIteration);
}
let page = document.querySelector(".page-info");
if (page) {
this.mountedView(page);
}
// else {
// console.error("初始化失败:", "站点信息不存在");
// }
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) {
// 添加 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);
}
},
/**
* 本文总访客量
*/
addtotalPageView() {
let pageView = document.querySelector(".page_total_view");
if (pageView) {
// 添加 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);
}
},
/**
* 挂载目标到页面上
*/
mountedView(template) {
let i = 0;
let parentElement = document.querySelector(this.moutedParentEvent);
if (parentElement) {
if (!this.isMountedView(template, parentElement)) {
parentElement.appendChild(template);
}
} else {
let interval = setInterval(() => {
let parentElement = document.querySelector(this.moutedParentEvent);
if (parentElement) {
if (!this.isMountedView(template, parentElement)) {
parentElement.appendChild(template);
clearInterval(interval);
}
} else if (i > 1 * 10) {
// 10 秒后清除
clearInterval(interval);
}
}, this.mountedIntervalTime);
// 绑定 beforeDestroy 生命钩子,清除定时器
this.$once("hook:beforeDestroy", () => {
clearInterval(interval);
interval = null;
});
}
},
//* 用于判断是否已经挂载到页面上 */
isMounted(template) {
let i = 0;
let interval = setInterval(() => {
let parentElement = document.querySelector(this.moutedParentEvent);
if (parentElement) {
if (!this.isMountedView(template, parentElement) && template) {
parentElement.appendChild(template);
clearInterval(interval);
}
} else if (i > 1 * 10) {
// 10 秒后清除
clearInterval(interval);
}
}, this.mountedIntervalTime);
// 绑定 beforeDestroy 生命钩子,清除定时器
this.$once("hook:beforeDestroy", () => {
clearInterval(interval);
interval = null;
});
},
/**
* 目标是否已经挂载在页面上
*/
isMountedView(element, parentElement) {
if (element) {
if (element.parentNode == parentElement) {
return true;
} else {
return false;
}
} else {
return false;
}
},
},
};
</script>
<style>
.view-data {
color: #999;
margin-left: 3px;
}
.page-info {
display: inline-block;
}
.page-hide{
display: none;
/* position: fixed;
top: 96px;
right: 450px;
background-color: red; */
}
</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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# 使用
# 使用 WebInfo.vue 组件
打开 docs/README.md文件
注意
github直接下载的vuepress-theme-vdoing为 docs/index.md
移到最下方,添加如下内容:
<ClientOnly>
<WebInfo/>
</ClientOnly>
2
3
# 使用 PageInfo.vue 组件
在 docs/.vuepress/config.js(新版是 config.ts)的 plugins 中添加配置。
module.exports = {
plugins: [
{
name: 'custom-plugins',
globalUIComponents: ["PageInfo"] // 2.x 版本 globalUIComponents 改名为 clientAppRootComponentFiles
}
]
}
2
3
4
5
6
7
8
# 首页站点信息配置
上面都按照步骤写好代码、使用组件了,那么就可以走最后一步配置我们的站点信息。
进入到 docs/.vuepress/config.js(新版为 config.ts)文件。
引入之前写好的工具代码文件:(路径要准确,这里仅仅是模板)
const { readFileList, readTotalFileWords, readEachFileWords } = require('./webSiteInfo/readFile');
在 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
属性配置的具体介绍请看 属性配置 (opens new window)。
# 将pageInfo添加到源码中
vupress默认主题的基础上使用vdoing主题会有刷新页面文章页站点信息在页面上不显示的问题,可修改vdoing主题源码,然后用patch-package打补丁
在node_modules\vuepress-theme-vdoing\util中添加busuanzi.js,[内容见网站信息工具代码busuanzi.js](#在 webSiteInfo 目录下创建
busuanzi.js
文件,这个文件用于 获取访问量)修改node_modules\vuepress-theme-vdoing\components\ArticleInfo.vue内容如下
<template>
<div class="articleInfo-wrap">
<div class="articleInfo">
<!-- 面包屑 -->
<ul class="breadcrumbs" v-if="classify1 && classify1 !== '_posts'">
<li>
<router-link to="/" class="iconfont icon-home" title="首页" />
</li>
<li v-for="item in classifyList" :key="item">
<!-- 跳目录页 -->
<router-link v-if="cataloguePermalink" :to="getLink(item)">{{
item
}}</router-link>
<!-- 跳分类页 -->
<router-link
v-else-if="$themeConfig.category !== false"
:to="`/categories/?category=${encodeURIComponent(item)}`"
title="分类"
>{{ item }}</router-link
>
<!-- 没有跳转 -->
<span v-else>{{ item }}</span>
</li>
</ul>
<!-- 作者&日期 -->
<div class="info">
<div class="author iconfont icon-touxiang" title="作者" v-if="author">
<a
:href="author.href || author.link"
v-if="
author.href || (author.link && typeof author.link === 'string')
"
target="_blank"
class="beLink"
title="作者"
>{{ author.name }}</a
>
<a v-else href="javascript:;">{{ author.name || author }}</a>
</div>
<div class="date iconfont icon-riqi" title="创建时间" v-if="date">
<a href="javascript:;">{{ date }}</a>
</div>
<!-- 当前文章页字数 -->
<div class="book-words iconfont icon-book" style="float: left; margin-left: 20px; font-size: 0.8rem;" title="文章字数">
<a href="javascript:;" style="margin-left: 3px; color: #888">{{ wordsCount }}</a>
</div>
<!-- 预计阅读时间 -->
<div class="reading-time iconfont icon-shijian" style="float: left; margin-left: 20px; font-size: 0.8rem;" title="预计阅读时间">
<a href="javascript:;" style="margin-left: 3px; color: #888">{{ readTimeCount }}</a>
</div>
<!-- 文章页访问量 -->
<div class="page-view iconfont icon-view" style="float: left; margin-left: 20px; font-size: 0.8rem;" title="文章访问量">
<a href="javascript:;" id="busuanzi_page_pv" class="view-data">
<i title="正在获取..." class="loading iconfont icon-loading"></i>
</a>
</div>
<!-- 本文总访客量 -->
<div class="page_total_view iconfont icon-tongji" style="float: left; margin-left: 20px; font-size: 0.8rem;" title="本文总访客量">
<a href="javascript:;" id="busuanzi_page_uv" class="view-data">
<i title="正在获取..." class="loading iconfont icon-loading"></i>
</a>
</div>
<div
class="date iconfont icon-wenjian"
title="分类"
v-if="
$themeConfig.category !== false &&
!(classify1 && classify1 !== '_posts') &&
categories
"
>
<router-link
:to="`/categories/?category=${encodeURIComponent(item)}`"
v-for="(item, index) in categories"
:key="index"
>{{ item + ' ' }}</router-link
>
</div>
</div>
</div>
</div>
</template>
<script>
import fetch from '../util/busuanzi.js'
export default {
data() {
return {
date: '',
classify1: '',
classifyList: [],
cataloguePermalink: '',
author: null,
categories: [],
wordsCount: 0,
readTimeCount: 0,
mountedIntervalTime: 1000,
moutedParentEvent: ".articleInfo-wrap > .articleInfo > .info"
}
},
created() {
this.getPageInfo()
},
mounted() {
this.$nextTick(function () {
this.initPageInfo();
})
},
watch: {
'$route.path'() {
this.classifyList = []
this.getPageInfo()
this.initPageInfo()
}
},
methods: {
getPageInfo() {
const pageInfo = this.$page
const { relativePath } = pageInfo
const { sidebar } = this.$themeConfig
// 分类采用解析文件夹地址名称的方式 (即使关闭分类功能也可以正确跳转目录页)
const relativePathArr = relativePath.split('/')
// const classifyArr = relativePathArr[0].split('.')
relativePathArr.forEach((item, index) => {
const nameArr = item.split('.')
if (index !== relativePathArr.length - 1) {
if (nameArr === 1) {
this.classifyList.push(nameArr[0])
} else {
const firstDotIndex = item.indexOf('.');
this.classifyList.push(item.substring(firstDotIndex + 1) || '')
}
}
})
this.classify1 = this.classifyList[0]
const cataloguePermalink = sidebar && sidebar.catalogue ? sidebar.catalogue[this.classify1] : ''// 目录页永久链接
const author = this.$frontmatter.author || this.$themeConfig.author // 作者
let date = (pageInfo.frontmatter.date || '').split(' ')[0] // 文章创建时间
// 获取页面frontmatter的分类(碎片化文章使用)
const { categories } = this.$frontmatter
this.date = date
this.cataloguePermalink = cataloguePermalink
this.author = author
this.categories = categories
},
getLink(item) {
const { cataloguePermalink } = this
if (item === cataloguePermalink) {
return cataloguePermalink
}
return `${cataloguePermalink}${cataloguePermalink.charAt(cataloguePermalink.length - 1) === '/'
? ''
: '/'
}#${item}`
},
/**
* 初始化页面信息
*/
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.wordsCount = itemFile.wordsCount;
if (readingTime || readingTime === undefined) {
this.readTimeCount = itemFile.readingTime;
}
}
});
} catch (error) {
// console.error("获取浏览量失败:", error);
}
}
if (pageView || pageView === undefined) {
this.addPageView();
this.addtotalPageView()
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) {
// 添加 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);
}
},
/**
* 本文总访客量
*/
addtotalPageView() {
let pageView = document.querySelector(".page_total_view");
if (pageView) {
// 添加 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);
}
},
}
}
</script>
<style lang='stylus' scoped>
@require '../styles/wrapper.styl'
.theme-style-line
.articleInfo-wrap
.articleInfo
padding-top 0.5rem
.articleInfo-wrap
@extend $wrapper
position relative
z-index 1
color #888
.articleInfo
overflow hidden
font-size 0.92rem
.breadcrumbs
margin 0
padding 0
overflow hidden
display inline-block
line-height 2rem
@media (max-width 960px)
width 100%
li
list-style-type none
float left
padding-right 5px
&:after
content '/'
margin-left 5px
color #999
&:last-child
&:after
content ''
a
color #888
&:before
font-size 0.92rem
&:hover
color $accentColor
.icon-home
text-decoration none
.info
float right
line-height 32px
@media (max-width 960px)
float left
div
float left
margin-left 20px
font-size 0.8rem
@media (max-width 960px)
margin 0 20px 0 0
&:before
margin-right 3px
a
color #888
&:hover
text-decoration none
a.beLink
&:hover
color $accentColor
text-decoration underline
.view-data {
color: #999;
margin-left: 3px;
}
</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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
打补丁,可参考 patch-package使用 (opens new window)
yarn patch-package vuepress-theme-vdoing
1
- 01
- element-plus多文件手动上传 原创11-03
- 02
- TrueLicense 创建及安装证书 原创10-25
- 03
- 手动修改迅捷配置 原创09-03
- 04
- 安装 acme.sh 原创08-29
- 05
- zabbix部署 原创08-20