新增页面:新增文章统计页面

# 前言

最开始参考的 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

# 更改语言并添加到菜单

  1. 刚才新建的 index.md 中,如果是用命令创建, title 一行是英文,改成对应的中文就可以了;
  2. themes\shoka\languages\zh-CN.yml 中,找到 menu 一行,新增 statistics: 文章统计
  3. 在主题的 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 %}
  1. 其中 tags-chartdata-length="10" 属性表示仅显示排名前 10 的标签。
  2. js 引入的顺序不要错,不然会出错

# 解决 pjax 加载问题

如果做到此步,就已经成功了,但是会发现,第一次会加载不出来,显示空白,只有再次点击或者刷新后才会显示,下面来解决此问题

  1. 在主题的 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
  1. 找到 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,
  2. 找到 themes\shoka\scripts\helpers\assets.js ,找到大概 50 行,新增 echartsconfig

    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

完成!

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Lavender 微信支付

微信支付

Lavender 支付宝

支付宝