新增页面:新增文章统计页面
# 前言
最开始参考的 matery 主题的统计页面,后来看到 Eurkon 大佬的文章,因此,本文章根据大佬的 Hexo 博客文章统计图适配本主题而成,改进了一些地方(pjax 问题、明暗模式问题),还留存一点 bug(黑暗模式刷新后字体会变成日间模式的颜色,未解决)
参考文章:Hexo 博客文章统计图
# 实现
# 新建页面
hexo new page statistics |
在根目录下的 source
下新建文件夹,命名为 statistics
,并在该目录下新建文件: index.md
,填入以下内容:
--- | |
title: 文章统计 | |
date: 2021-12-28 20:30:00 | |
--- |
# 安装 cheerio
在博客根目录下打开 git bash
,安装 cheerio
:
npm i cheerio --save |
# 更改语言并添加到菜单
- 刚才新建的
index.md
中,如果是用命令创建,title
一行是英文,改成对应的中文就可以了; - 在
themes\shoka\languages\zh-CN.yml
中,找到menu
一行,新增statistics: 文章统计
; - 在主题的
yml
文件里,在menu
里添加到菜单中
# 文章统计代码(已适配明暗模式)
在 themes\shoka\scripts\helpers\
目录下新建文件命名为 charts.js
,填入以下内容(注意修改下面有注释的开始统计时间):
'use strict'; | |
const cheerio = require('cheerio') | |
const moment = require('moment') | |
hexo.extend.filter.register('after_render:html', function (locals) { | |
const $ = cheerio.load(locals) | |
const calendar = $('#posts-calendar') | |
const post = $('#posts-chart') | |
const tag = $('#tags-chart') | |
const category = $('#categories-chart') | |
let htmlEncode = false | |
if (calendar.length > 0 || post.length > 0 || tag.length > 0 || category.length > 0) { | |
if (calendar.length > 0 && $('#postsCalendar').length === 0) { | |
if (calendar.attr('data-encode') === 'true') htmlEncode = true | |
calendar.after(postsCalendar()) | |
} | |
if (post.length > 0 && $('#postsChart').length === 0) { | |
if (post.attr('data-encode') === 'true') htmlEncode = true | |
post.after(postsChart()) | |
} | |
if (tag.length > 0 && $('#tagsChart').length === 0) { | |
if (tag.attr('data-encode') === 'true') htmlEncode = true | |
tag.after(tagsChart(tag.attr('data-length'))) | |
} | |
if (category.length > 0 && $('#categoriesChart').length === 0) { | |
if (category.attr('data-encode') === 'true') htmlEncode = true | |
category.after(categoriesChart()) | |
} | |
if (htmlEncode) { | |
return $.root().html().replace(/&#/g, '&#') | |
} else { | |
return $.root().html() | |
} | |
} else { | |
return locals | |
} | |
}, 15) | |
function postsCalendar () { | |
// calculate range. | |
const start_date = moment().subtract(1, 'years'); | |
const end_date = moment(); | |
const rangeArr = '["' + start_date.format('YYYY-MM-DD') + '", "' + end_date.format('YYYY-MM-DD') + '"]'; | |
// post and count map. | |
var dateMap = new Map(); | |
hexo.locals.get('posts').forEach(function (post) { | |
var date = post.date.format('YYYY-MM-DD'); | |
var count = dateMap.get(date); | |
dateMap.set(date, count == null || count == undefined ? 1 : count + 1); | |
}); | |
// loop the data for the current year, generating the number of post per day | |
var i = 0; | |
var datePosts = '['; | |
var day_time = 3600 * 24 * 1000; | |
for (var time = start_date; time <= end_date; time += day_time) { | |
var date = moment(time).format('YYYY-MM-DD'); | |
datePosts = (i === 0 ? datePosts + '["' : datePosts + ', ["') + date + '", ' | |
+ (dateMap.has(date) ? dateMap.get(date) : 0) + ']'; | |
i++; | |
} | |
datePosts += ']'; | |
return ` | |
<script id="postsCalendar"> | |
var color = document.documentElement.getAttribute('data-theme') === null ? '#000' : '#fff' | |
var postsCalendar = echarts.init(document.getElementById('posts-calendar'), 'light'); | |
var postsCalendarOption = { | |
textStyle: { | |
color: color | |
}, | |
title: { | |
top: 0, | |
text: '文章发布日历', | |
left: 'center', | |
textStyle: { | |
color: color | |
} | |
}, | |
tooltip: { | |
formatter: function (obj) { | |
var value = obj.value; | |
return '<div style="font-size: 14px;">' + value[0] + ':' + value[1] + '</div>'; | |
} | |
}, | |
visualMap: { | |
show: true, | |
showLabel: true, | |
categories: [0, 1, 2, 3, 4], | |
calculable: true, | |
textStyle:{ | |
color: color | |
}, | |
inRange: { | |
symbol: 'rect', | |
color: ['#d7dbe2', '#fc9bd9', '#f838b2', '#c4067e', '#540336'] | |
}, | |
itemWidth: 12, | |
itemHeight: 12, | |
orient: 'horizontal', | |
left: 'center', | |
bottom: 80 | |
}, | |
calendar: { | |
left: 'center', | |
range: ${rangeArr}, | |
cellSize: [13, 13], | |
splitLine: { | |
show: true | |
}, | |
itemStyle: { | |
color: '#111', | |
borderColor: '#fff', | |
borderWidth: 2 | |
}, | |
yearLabel: { | |
show: false, | |
color: color | |
}, | |
monthLabel: { | |
nameMap: 'cn', | |
fontSize: 11, | |
color: color | |
}, | |
dayLabel: { | |
formatter: '{start} 1st', | |
nameMap: 'cn', | |
fontSize: 11, | |
color: color | |
} | |
}, | |
series: [{ | |
type: 'heatmap', | |
coordinateSystem: 'calendar', | |
calendarIndex: 0, | |
data: ${datePosts} | |
}] | |
}; | |
postsCalendar.setOption(postsCalendarOption); | |
window.addEventListener("resize", () => { | |
postsCalendar.resize(); | |
}); | |
</script>` | |
} | |
function postsChart () { | |
const startDate = moment('2020-08') // 开始统计的时间 | |
const endDate = moment() | |
const monthMap = new Map() | |
const dayTime = 3600 * 24 * 1000 | |
for (let time = startDate; time <= endDate; time += dayTime) { | |
const month = moment(time).format('YYYY-MM') | |
if (!monthMap.has(month)) { | |
monthMap.set(month, 0) | |
} | |
} | |
hexo.locals.get('posts').forEach(function (post) { | |
const month = post.date.format('YYYY-MM') | |
if (monthMap.has(month)) { | |
monthMap.set(month, monthMap.get(month) + 1) | |
} | |
}) | |
const monthArr = JSON.stringify([...monthMap.keys()]) | |
const monthValueArr = JSON.stringify([...monthMap.values()]) | |
return ` | |
<script id="postsChart"> | |
var color = document.documentElement.getAttribute('data-theme') === null ? '#000' : '#fff' | |
var postsChart = echarts.init(document.getElementById('posts-chart'), 'light'); | |
var postsOption = { | |
textStyle: { | |
color: color | |
}, | |
title: { | |
text: '文章发布统计图', | |
x: 'center', | |
textStyle: { | |
color: color | |
} | |
}, | |
tooltip: { | |
trigger: 'axis' | |
}, | |
xAxis: { | |
name: '日期', | |
type: 'category', | |
axisTick: { | |
show: false | |
}, | |
axisLine: { | |
show: true, | |
lineStyle: { | |
color: color | |
} | |
}, | |
data: ${monthArr} | |
}, | |
yAxis: { | |
name: '文章篇数', | |
type: 'value', | |
splitLine: { | |
show: false | |
}, | |
axisTick: { | |
show: false | |
}, | |
axisLine: { | |
show: true, | |
lineStyle: { | |
color: color | |
} | |
} | |
}, | |
series: [{ | |
name: '文章篇数', | |
type: 'line', | |
smooth: true, | |
lineStyle: { | |
width: 0 | |
}, | |
showSymbol: false, | |
itemStyle: { | |
opacity: 1, | |
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ | |
offset: 0, | |
color: 'rgba(128, 255, 165)' | |
}, | |
{ | |
offset: 1, | |
color: 'rgba(1, 191, 236)' | |
}]) | |
}, | |
areaStyle: { | |
opacity: 1, | |
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ | |
offset: 0, | |
color: 'rgba(128, 255, 165)' | |
}, { | |
offset: 1, | |
color: 'rgba(1, 191, 236)' | |
}]) | |
}, | |
data: ${monthValueArr}, | |
markLine: { | |
data: [{ | |
name: '平均值', | |
type: 'average' | |
}] | |
} | |
}] | |
}; | |
postsChart.setOption(postsOption); | |
window.addEventListener("resize", () => { | |
postsChart.resize(); | |
}); | |
</script>` | |
} | |
function tagsChart (len) { | |
const tagArr = [] | |
hexo.locals.get('tags').map(function (tag) { | |
tagArr.push({ name: tag.name, value: tag.length }) | |
}) | |
tagArr.sort((a, b) => { return b.value - a.value }) | |
let dataLength = Math.min(tagArr.length, len) || tagArr.length | |
const tagNameArr = [] | |
const tagCountArr = [] | |
for (let i = 0; i < dataLength; i++) { | |
tagNameArr.push(tagArr[i].name) | |
tagCountArr.push(tagArr[i].value) | |
} | |
const tagNameArrJson = JSON.stringify(tagNameArr) | |
const tagCountArrJson = JSON.stringify(tagCountArr) | |
return ` | |
<script id="tagsChart"> | |
var color = document.documentElement.getAttribute('data-theme') === null ? '#000' : '#fff' | |
var tagsChart = echarts.init(document.getElementById('tags-chart'), 'light'); | |
var tagsOption = { | |
textStyle: { | |
color: color | |
}, | |
title: { | |
text: 'Top ${dataLength} 标签统计图', | |
x: 'center', | |
textStyle: { | |
color: color | |
} | |
}, | |
tooltip: {}, | |
xAxis: { | |
name: '标签', | |
type: 'category', | |
axisTick: { | |
show: false | |
}, | |
axisLine: { | |
show: true, | |
lineStyle: { | |
color: color | |
} | |
}, | |
data: ${tagNameArrJson} | |
}, | |
yAxis: { | |
name: '文章篇数', | |
type: 'value', | |
splitLine: { | |
show: false | |
}, | |
axisTick: { | |
show: false | |
}, | |
axisLine: { | |
show: true, | |
lineStyle: { | |
color: color | |
} | |
} | |
}, | |
series: [{ | |
name: '文章篇数', | |
type: 'bar', | |
data: ${tagCountArrJson}, | |
itemStyle: { | |
opacity: 1, | |
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ | |
offset: 0, | |
color: 'rgba(128, 255, 165)' | |
}, | |
{ | |
offset: 1, | |
color: 'rgba(1, 191, 236)' | |
}]) | |
}, | |
emphasis: { | |
itemStyle: { | |
opacity: 1, | |
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ | |
offset: 0, | |
color: 'rgba(128, 255, 195)' | |
}, | |
{ | |
offset: 1, | |
color: 'rgba(1, 211, 255)' | |
}]) | |
} | |
}, | |
markLine: { | |
data: [{ | |
name: '平均值', | |
type: 'average' | |
}] | |
} | |
}] | |
}; | |
tagsChart.setOption(tagsOption); | |
window.addEventListener("resize", () => { | |
tagsChart.resize(); | |
}); | |
</script>` | |
} | |
function categoriesChart () { | |
const categoryArr = [] | |
hexo.locals.get('categories').map(function (category) { | |
categoryArr.push({ name: category.name, value: category.length }) | |
}) | |
categoryArr.sort((a, b) => { return b.value - a.value }); | |
const categoryArrJson = JSON.stringify(categoryArr) | |
return ` | |
<script id="categoriesChart"> | |
var color = document.documentElement.getAttribute('data-theme') === null ? '#000' : '#fff' | |
var categoriesChart = echarts.init(document.getElementById('categories-chart'), 'light'); | |
var categoriesOption = { | |
textStyle: { | |
color: color | |
}, | |
title: { | |
text: '文章分类统计图', | |
x: 'center', | |
textStyle: { | |
color: color | |
} | |
}, | |
legend: { | |
top: 'bottom', | |
textStyle: { | |
color: color | |
} | |
}, | |
tooltip: { | |
trigger: 'item', | |
formatter: "{a} <br/>{b} : {c} ({d}%)" | |
}, | |
series: [{ | |
name: '文章篇数', | |
type: 'pie', | |
radius: [30, 80], | |
center: ['50%', '50%'], | |
roseType: 'area', | |
label: { | |
formatter: "{b} : {c} ({d}%)" | |
}, | |
data: ${categoryArrJson}, | |
itemStyle: { | |
emphasis: { | |
shadowBlur: 10, | |
shadowOffsetX: 0, | |
shadowColor: 'rgba(255, 255, 255, 0.5)' | |
} | |
} | |
}] | |
}; | |
categoriesChart.setOption(categoriesOption); | |
window.addEventListener("resize", () => { | |
categoriesChart.resize(); | |
}); | |
</script>` | |
} |
# 使用统计图(已适配明暗模式)
在刚刚新建的 statistics
下的 index.md
中新增内容:
{% raw %} | |
<script src="https://cdn.jsdelivr.net/npm/echarts@5.2.2/dist/echarts.min.js"></script> | |
<script> | |
function switchPostChart () { | |
// 这里为了统一颜色选取的是 “明暗模式” 下的两种字体颜色,也可以自己定义 | |
let color = document.documentElement.getAttribute('data-theme') === null ? '#fff' : '#000' | |
if (document.getElementById('posts-calendar')) { | |
let postsCalendarNew = postsCalendarOption | |
postsCalendarNew.textStyle.color = color | |
postsCalendarNew.title.textStyle.color = color | |
postsCalendarNew.visualMap.textStyle.color = color | |
postsCalendarNew.calendar.itemStyle.color = color | |
postsCalendarNew.calendar.yearLabel.color = color | |
postsCalendarNew.calendar.monthLabel.color = color | |
postsCalendarNew.calendar.dayLabel.color = color | |
postsCalendar.setOption(postsCalendarNew) | |
} | |
if (document.getElementById('posts-chart')) { | |
let postsOptionNew = postsOption | |
postsOptionNew.textStyle.color = color | |
postsOptionNew.title.textStyle.color = color | |
postsOptionNew.xAxis.axisLine.lineStyle.color = color | |
postsOptionNew.yAxis.axisLine.lineStyle.color = color | |
postsChart.setOption(postsOptionNew) | |
} | |
if (document.getElementById('tags-chart')) { | |
let tagsOptionNew = tagsOption | |
tagsOptionNew.textStyle.color = color | |
tagsOptionNew.title.textStyle.color = color | |
tagsOptionNew.xAxis.axisLine.lineStyle.color = color | |
tagsOptionNew.yAxis.axisLine.lineStyle.color = color | |
tagsChart.setOption(tagsOptionNew) | |
} | |
if (document.getElementById('categories-chart')) { | |
let categoriesOptionNew = categoriesOption | |
categoriesOptionNew.textStyle.color = color | |
categoriesOptionNew.title.textStyle.color = color | |
categoriesOptionNew.legend.textStyle.color = color | |
categoriesChart.setOption(categoriesOptionNew) | |
} | |
} | |
document.getElementsByClassName("theme")[0].addEventListener("click", function () { setTimeout(switchPostChart, 100) }) | |
</script> | |
<!-- 文章发布日历 --> | |
<div id="posts-calendar" style="border-radius: 8px; height: 300px; padding: 10px;"></div> | |
<!-- 文章发布时间统计图 --> | |
<div id="posts-chart" style="border-radius: 8px; height: 300px; padding: 10px;"></div> | |
<!-- 文章标签统计图 --> | |
<div id="tags-chart" data-length="10" style="border-radius: 8px; height: 300px; padding: 10px;"></div> | |
<!-- 文章分类统计图 --> | |
<div id="categories-chart" style="border-radius: 8px; height: 300px; padding: 10px;"></div> | |
{% endraw %} |
- 其中
tags-chart
的data-length="10"
属性表示仅显示排名前 10 的标签。 js
引入的顺序不要错,不然会出错
# 解决 pjax 加载问题
如果做到此步,就已经成功了,但是会发现,第一次会加载不出来,显示空白,只有再次点击或者刷新后才会显示,下面来解决此问题
- 在主题的
yml
配置文件中,找到最下面的vendors
,在js
一栏新增echarts
引入:
vendors: | |
js: | |
…… | |
echarts: npm/echarts@5.2.2/dist/echarts.min.js |
代码:
echarts: npm/echarts@5.2.2/dist/echarts.min.js |
找到
themes\shoka\scripts\generaters\script.js
,大概 24 行,找到js
,新增echarts
内容,内容为下:auto_scroll: theme.auto_scroll,
js: {
valine: theme.vendors.js.valine,
chart: theme.vendors.js.chart,
copy_tex: theme.vendors.js.copy_tex,
fancybox: theme.vendors.js.fancybox,
echarts: theme.vendors.js.echarts, // 新增
},
css: {
代码:
echarts: theme.vendors.js.echarts,
找到
themes\shoka\scripts\helpers\assets.js
,找到大概 50 行,新增echarts
的config
:if (!config) return '';
//Get a font list from config
let vendorJs = ['pace', 'pjax', 'fetch', 'anime', 'algolia', 'instantsearch', 'lazyload', 'quicklink'].map(item => { // 这里在 quicklink 后新增
if (config[item]) {
return config[item];
}
return '';
});
改后
if (!config) return '';
//Get a font list from config
let vendorJs = ['pace', 'pjax', 'fetch', 'anime', 'algolia', 'instantsearch', 'lazyload', 'quicklink', 'echarts'].map(item => {
if (config[item]) {
return config[item];
}
return '';
});
# 其他
如果想要修改图表的样式,请参考 echarts
官网:https://echarts.apache.org/zh/index.html
完成!