为了更好的使用体验,现将本站对solitude主题的全部魔改文件公示于此,希望能给一些想魔改但无从下手的小白提供一些帮助

2025.05.04 更新说明:本文修改内容适用于 solitude 3.0.19 版本

评论链接安全跳转

路径:
solitude/layout/includes/widgets/third-party/comments/twikoo.pug

为实现评论区链接安全跳转提醒,将该文件onCommentLoaded之后一行进行修改,为保证缩进一致,此处贴出全部文件(使用前请先安装hexo安全跳转插件)

- const { envId, region, option ,accessToken } = theme.twikoo
- const { lazyload, count, use,commentBarrage } = theme.comment

script().
    (() => {
        const getCount = () => {
            const ele = document.querySelectorAll('.twikoo-count')
            if (!ele) return
            twikoo.getCommentsCount({
                envId: '!{envId}',
                region: '!{region}',
                urls: [window.location.pathname],
                includeReply: false
            }).then(res => {
                ele.forEach(item => item.textContent = res[0].count)
            }).catch(err => {
                console.error(err)
            })
        }
        const init = () => {
            twikoo.init(Object.assign({
                el: '#twikoo-wrap',
                envId: '!{envId}',
                region: '!{region}',
                path: window.location.pathname,
                onCommentLoaded: () => {
                    GLOBAL_CONFIG.lightbox && utils.lightbox(document.querySelectorAll('#twikoo .tk-content img:not(.tk-owo-emotion)'))
                    const processLinks = () => {
                        const container = document.querySelector('#twikoo .tk-comments-container');
                        if (!container) return;

                        const links = container.querySelectorAll('a');
                        links.forEach((link) => {
                            const href = link.getAttribute('href');
                            if (!href) return;

                            if (!href.startsWith(window.location.origin) && !link.hasAttribute('data-fancybox')) {
                                const encodedHref = btoa(href);
                                const newHref = `/go.html?i=${encodedHref}`;
                                link.setAttribute('href', newHref);
                                link.setAttribute('rel', 'external nofollow noopener noreferrer');
                                link.setAttribute('target', '_blank');
                            }
                        });
                    };
                    processLinks();
                }
            }, !{JSON.stringify(option)}))

            !{count ? ' && getCount()' : ''}
            sco.owoBig({
                body: '.OwO-body',
                item: '.OwO-items li'
            })

            !{commentBarrage} && barrageTwikoo()
        }

        const loadTwikoo = () => {
            if (typeof twikoo === 'object') setTimeout(init,0)
            else utils.getScript('!{url_for(theme.cdn.twikoo)}').then(init)
        }

        if ('!{use[0]}' === 'Twikoo' || !{lazyload}) {
            if (!{lazyload}) utils.loadComment(document.getElementById('twikoo-wrap'), loadTwikoo)
            else loadTwikoo()
        } else {
            window.loadTwoComment = loadTwikoo
        }
    })()

if commentBarrage
    script.
        async function barrageTwikoo() {
            await fetch("!{envId}", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json"
                },
                body: JSON.stringify({
                    event: "COMMENT_GET",
                    accessToken: "!{accessToken}",
                    url: window.location.pathname
                })
            }).then(async res => {
                if (!res.ok) throw new Error("HTTP error! status: " + res.status)
                const data = await res.json();
                const init = () => {
                    initializeCommentBarrage((data.data).map(item => Object.assign({
                        content: item.comment,
                        nick: item.nick,
                        mailMd5: item.mailMd5,
                        id: item.id
                    })))
                }
                if (typeof initializeCommentBarrage === "undefined") await utils.getScript('!{url_for(theme.cdn.commentBarrage)}').then(init)
                else init()
            }).catch(error => console.error("An error occurred while fetching comments: ", error))
        }

修改的文件内容中对本站有自行修改过的独立适配,若您在安装安全跳转插件后使用默认配置,请将本文件中第39行内容替换为

const newHref = `/go.html?u=${encodedHref}`;

加载动画

颜色与闪烁速度

路径:
solitude/source/css/_layout/fullpage.styl

.loading-bg部分针对加载动画颜色的修改(增加透明度)

background var(--loading-bg)

.loading-img部分针对加载动画头像闪烁速度的修改

animation-duration 0.5s

背景颜色(透明度)

路径:
solitude/source/css/_mode/index.styl

针对日夜双模式加载动画的颜色(透明度)的修改

[data-theme=dark]

--loading-bg #000000dd

[data-theme=light]

--loading-bg #ffffffdd

文章统计

原教程与更多魔改内容:

  1. 创建页面

    hexo new page charts
  2. 配置文件引入部分引入:

    inject:
      head:
        - <script src="https://npm.elemecdn.com/echarts@4.9.0/dist/echarts.min.js"></script>
  3. 新建文件路径:
    solitude/scripts/helper/charts.js

    const cheerio = require('cheerio')
    const moment = require('moment')
    
    hexo.extend.filter.register('after_render:html', function (locals) {
      const $ = cheerio.load(locals)
      const post = $('#posts-chart')
      const tag = $('#tags-chart')
      const category = $('#categories-chart')
      const htmlEncode = false
    
      if (post.length > 0 || tag.length > 0 || category.length > 0) {
        if (post.length > 0 && $('#postsChart').length === 0) {
          if (post.attr('data-encode') === 'true') htmlEncode = true
          post.after(postsChart(post.attr('data-start')))
        }
        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(category.attr('data-parent')))
        }
    
        if (htmlEncode) {
          return $.root().html().replace(/&amp;#/g, '&#')
        } else {
          return $.root().html()
        }
      } else {
        return locals
      }
    }, 15)
    
    function postsChart (startMonth) {
      const startDate = moment(startMonth || '2020-01')
      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') === 'light' ? '#4c4948' : 'rgba(255,255,255,0.7)'
        var postsChart = echarts.init(document.getElementById('posts-chart'), 'light');
        var postsOption = {
          title: {
            text: '文章发布统计图',
            x: 'center',
            textStyle: {
              color: color
            }
          },
          tooltip: {
            trigger: 'axis'
          },
          xAxis: {
            name: '日期',
            type: 'category',
            boundaryGap: false,
            nameTextStyle: {
              color: color
            },
            axisTick: {
              show: false
            },
            axisLabel: {
              show: true,
              color: color
            },
            axisLine: {
              show: true,
              lineStyle: {
                color: color
              }
            },
            data: ${monthArr}
          },
          yAxis: {
            name: '文章篇数',
            type: 'value',
            nameTextStyle: {
              color: color
            },
            splitLine: {
              show: false
            },
            axisTick: {
              show: false
            },
            axisLabel: {
              show: true,
              color: color
            },
            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',
                label: {
                  color: color
                }
              }]
            }
          }]
        };
        postsChart.setOption(postsOption);
        window.addEventListener('resize', () => { 
          postsChart.resize();
        });
        postsChart.on('click', 'series', (event) => {
          if (event.componentType === 'series') window.location.href = '/archives/' + event.name.replace('-', '/');
        });
      </script>`
    }
    
    function tagsChart (len) {
      const tagArr = []
      hexo.locals.get('tags').map(function (tag) {
        tagArr.push({ name: tag.name, value: tag.length, path: tag.path })
      })
      tagArr.sort((a, b) => { return b.value - a.value })
    
      const dataLength = Math.min(tagArr.length, len) || tagArr.length
      const tagNameArr = []
      for (let i = 0; i < dataLength; i++) {
        tagNameArr.push(tagArr[i].name)
      }
      const tagNameArrJson = JSON.stringify(tagNameArr)
      const tagArrJson = JSON.stringify(tagArr)
    
      return `
      <script id="tagsChart">
        var color = document.documentElement.getAttribute('data-theme') === 'light' ? '#4c4948' : 'rgba(255,255,255,0.7)'
        var tagsChart = echarts.init(document.getElementById('tags-chart'), 'light');
        var tagsOption = {
          title: {
            text: 'Top ${dataLength} 标签统计图',
            x: 'center',
            textStyle: {
              color: color
            }
          },
          tooltip: {},
          xAxis: {
            name: '标签',
            type: 'category',
            nameTextStyle: {
              color: color
            },
            axisTick: {
              show: false
            },
            axisLabel: {
              show: true,
              color: color,
              interval: 0
            },
            axisLine: {
              show: true,
              lineStyle: {
                color: color
              }
            },
            data: ${tagNameArrJson}
          },
          yAxis: {
            name: '文章篇数',
            type: 'value',
            splitLine: {
              show: false
            },
            nameTextStyle: {
              color: color
            },
            axisTick: {
              show: false
            },
            axisLabel: {
              show: true,
              color: color
            },
            axisLine: {
              show: true,
              lineStyle: {
                color: color
              }
            }
          },
          series: [{
            name: '文章篇数',
            type: 'bar',
            data: ${tagArrJson},
            itemStyle: {
              borderRadius: [5, 5, 0, 0],
              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: {
                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',
                label: {
                  color: color
                }
              }]
            }
          }]
        };
        tagsChart.setOption(tagsOption);
        window.addEventListener('resize', () => { 
          tagsChart.resize();
        });
        tagsChart.on('click', 'series', (event) => {
          if(event.data.path) window.location.href = '/' + event.data.path;
        });
      </script>`
    }
    
    function categoriesChart (dataParent) {
      const categoryArr = []
      let categoryParentFlag = false
      hexo.locals.get('categories').map(function (category) {
        if (category.parent) categoryParentFlag = true
        categoryArr.push({
          name: category.name,
          value: category.length,
          path: category.path,
          id: category._id,
          parentId: category.parent || '0'
        })
      })
      categoryParentFlag = categoryParentFlag && dataParent === 'true'
      categoryArr.sort((a, b) => { return b.value - a.value })
      function translateListToTree (data, parent) {
        let tree = []
        let temp
        data.forEach((item, index) => {
          if (data[index].parentId == parent) {
            let obj = data[index];
            temp = translateListToTree(data, data[index].id);
            if (temp.length > 0) {
              obj.children = temp
            }
            if (tree.indexOf())
              tree.push(obj)
          }
        })
        return tree
      }
      const categoryNameJson = JSON.stringify(categoryArr.map(function (category) { return category.name }))
      const categoryArrJson = JSON.stringify(categoryArr)
      const categoryArrParentJson = JSON.stringify(translateListToTree(categoryArr, '0'))
    
      return `
      <script id="categoriesChart">
        var color = document.documentElement.getAttribute('data-theme') === 'light' ? '#4c4948' : 'rgba(255,255,255,0.7)'
        var categoriesChart = echarts.init(document.getElementById('categories-chart'), 'light');
        var categoryParentFlag = ${categoryParentFlag}
        var categoriesOption = {
          title: {
            text: '文章分类统计图',
            x: 'center',
            textStyle: {
              color: color
            }
          },
          legend: {
            top: 'bottom',
            data: ${categoryNameJson},
            textStyle: {
              color: color
            }
          },
          tooltip: {
            trigger: 'item'
          },
          series: []
        };
        categoriesOption.series.push(
          categoryParentFlag ? 
          {
            nodeClick :false,
            name: '文章篇数',
            type: 'sunburst',
            radius: ['15%', '90%'],
            center: ['50%', '55%'],
            sort: 'desc',
            data: ${categoryArrParentJson},
            itemStyle: {
              borderColor: '#fff',
              borderWidth: 2,
              emphasis: {
                focus: 'ancestor',
                shadowBlur: 10,
                shadowOffsetX: 0,
                shadowColor: 'rgba(255, 255, 255, 0.5)'
              }
            }
          }
          :
          {
            name: '文章篇数',
            type: 'pie',
            radius: [30, 80],
            roseType: 'area',
            label: {
              color: color,
              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();
        });
        categoriesChart.on('click', 'series', (event) => {
          if(event.data.path) window.location.href = '/' + event.data.path;
        });
      </script>`
    }
  4. 在页面中显示

    <!-- 文章发布时间统计图 -->
    <div id="posts-chart" data-start="2021-01" 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" data-parent="true" style="border-radius: 8px; height: 300px; padding: 10px;"></div>

标题宽度修改

为h1增加上下宽度、为h2增加下宽度

路径:
solitude/source/css/_layout/article-container.styl

\\ 193行起
    h1
      font-size 1.5rem
      line-height 1.3
      padding-top 1rem
      padding-bottom 1rem

    h2
      font-size 1.3rem
      line-height 1.3
      border-top 1px dashed var(--efu-theme-op)
      padding-top 1rem
      padding-bottom 1rem

移动端文章页边距调整

路径:
solitude/source/css/_layout/article-container.styl

\\第6行
      padding 1rem .5rem

路径:
solitude/source/css/_global/index.styl

\\409行
    padding 0 .5rem

即刻说说支持换行与加粗

路径:
solitude/layout/includes/page/brevity.pug文件,修改第19行

!= item.content

路径:
solitude/layout/includes/widgets/home/bbTimeList.pug,修改第7行

| #{item.content.replace(/<[^>]*>/g, '')}

后续撰写使用<br>进行换行,使用<b>加粗文字</b>实现文字加粗标重

文章本地AI调整

本部分内容调整鸣谢:清羽飞扬

另外感谢ChatGPT(?

具体实现的效果可参考本站

js调整

路径:
solitude/source/js/post_ai.js

class AIPostRenderer {
  static ANIMATION_DELAY_MS = 40; // 每个字符之间的延迟(单位:ms)
  static AI_EXPLANATION_SELECTOR = ".ai-explanation"; // 解释容器选择器
  static AI_TAG_SELECTOR = ".ai-tag"; // 标签选择器

  constructor() {
    this.startTextAnimation = this.startTextAnimation.bind(this);
    this.animationFrame = null;
    this.isDeleting = false; // 是否处于删除阶段
  }

  init() {
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", this.initialize.bind(this));
    } else {
      this.initialize();
    }
  }

  initialize() {
    this.cacheElements();
    if (this.validateContent()) {
      this.renderAIContent();
    }
  }

  cacheElements() {
    this.refs = new WeakMap();
    this.refs.set(document, {
      explanationElement: document.querySelector(AIPostRenderer.AI_EXPLANATION_SELECTOR),
      tagElement: document.querySelector(AIPostRenderer.AI_TAG_SELECTOR)
    });
    const { explanationElement, tagElement } = this.refs.get(document) || {};
    this.explanationElement = explanationElement;
    this.tagElement = tagElement;
  }

  validateContent() {
    return (
      this.explanationElement &&
      this.tagElement &&
      this.aiContent.length &&
      !this.isAnimating
    );
  }

  renderAIContent() {
    this.prepareAnimation();
    // 延迟3秒后,开始真正执行删除和打字动画
    setTimeout(() => {
      this.explanationElement.style.display = "block"; // 显示解释容器
      this.explanationElement.classList.add("fast-blink"); // 开始快闪
      this.animationFrame = requestAnimationFrame(() =>
        this.startTextAnimation(0)
      );
    }, 3000);
  }

  prepareAnimation() {
    this.isAnimating = true;
    this.isDeleting = true; // 初始化设为删除模式
    this.tagElement.classList.add("loadingAI");
    // 注意这里不加 fast-blink,保持3000ms期间慢闪
  }

  startTextAnimation(index) {
    if (this.isDeleting) {
      // 删除阶段
      const currentText = this.explanationElement.textContent;
      if (currentText.length > 0) {
        this.explanationElement.textContent = currentText.slice(0, -1); // 删除最后一个字
        setTimeout(() => {
          this.animationFrame = requestAnimationFrame(() =>
            this.startTextAnimation(index)
          );
        }, AIPostRenderer.ANIMATION_DELAY_MS);
      } else {
        this.isDeleting = false; // 删除完成,开始打字
        this.startTextAnimation(0);
      }
    } else {
      // 打字阶段
      if (index >= this.aiContent.length) {
        this.completeAnimation(); // 打字完成
      } else {
        this.explanationElement.textContent += this.aiContent[index]; // 添加下一个字符
        setTimeout(() => {
          this.animationFrame = requestAnimationFrame(() =>
            this.startTextAnimation(index + 1)
          );
        }, AIPostRenderer.ANIMATION_DELAY_MS);
      }
    }
  }

  completeAnimation() {
    cancelAnimationFrame(this.animationFrame);
    this.isAnimating = false;
    this.tagElement.classList.remove("loadingAI");
    this.explanationElement.classList.remove("fast-blink"); // 打字完成后恢复慢闪
    const event = new CustomEvent("aiRenderComplete", {
      detail: { element: this.explanationElement }
    });
    document.dispatchEvent(event);
  }

  get aiContent() {
    return PAGE_CONFIG?.ai_text || "";
  }
}

// 单例模式,避免重复初始化
const aiPostRenderer = (() => {
  let instance;
  return () => {
    if (!instance) {
      instance = new AIPostRenderer();
      instance.init();
    }
    return instance;
  };
})();

// 初始化
const ai = aiPostRenderer();

css调整

路径:
solitude/source/css/_post/index.styl

if hexo-config('comment.commentBarrage')
  @import 'commentBarrage'

@import "copyright"

@import "meta"

@import "relatedPost"

@import "tools"

@import "pagination"

if hexo-config('google_adsense.enable')
  @import "ads.styl"

if hexo-config('post.ai.enable')
  .post-ai
    background var(--efu-secondbg)
    border-radius 12px
    padding 12px
    line-height 1.3
    border var(--style-border-always)
    margin-top 16px
    min-height 101.22px
    box-shadow var(--efu-shadow-border)

    +maxWidth768()
      margin-top 0

    .ai-title
      display flex
      color var(--efu-lighttext)
      border-radius 8px
      align-items center
      user-select none

      .ai-title-left
        display flex
        align-items center
        color var(--efu-lighttext)

        i.ai-title-icon
          width 24px
          height 24px
          display flex
          background var(--efu-lighttext)
          color var(--efu-card-bg)
          font-size 14px
          border-radius 20px
          justify-content center
          align-items center

        .ai-title-text
          font-weight 700
          margin-left 8px
          line-height 1
          font-size 14px

      .ai-tag
        font-size 12px
        background-color var(--efu-lighttext)
        box-shadow var(--efu-shadow-main)
        color var(--efu-card-bg)
        font-weight 700
        border-radius 12px
        margin-left auto
        line-height 12px
        padding 6px 8px
        display flex
        align-items center
        justify-content center
        transition .3s

        &.loadingAI
          animation-duration 2s
          animation-name AILoading
          animation-iteration-count infinite
          animation-direction alternate

    .ai-explanation
      margin-top 12px
      overflow hidden
      padding 8px 12px
      background var(--efu-card-bg)
      border-radius 8px
      border var(--style-border-always)
      font-size 15px
      line-height 1.4
      display none
      text-align left

      &:after
        content ''
        display inline-block
        width 10px
        height 3px
        margin-left 2px
        background var(--efu-lighttext)
        vertical-align bottom
        animation blink-underline 1s ease-in-out infinite
        transition all .3s
        position relative
        bottom 3px

        @keyframes blink-underline
          0%, 100%
            opacity 1
          50%
            opacity 0

      .blinking-cursor
        background-color var(--efu-lighttext)
        width 14px
        height 14px
        border-radius 16px
        display inline-block
        vertical-align middle
        animation blinking-cursor 2s infinite
        margin-left 4px
        margin-bottom 3px
        transform scale(.6)

  .char
    display inline-block
    opacity 0
    animation chat-float .5s ease forwards;

  @keyframes chat-float
    0% 
      opacity 0
      transform translateY(20px)
    100% 
        opacity 1
        transform translateY(0)
  
  @keyframes blink-underline-fast
    0%, 100%
      opacity 1
    50%
      opacity 0

  .ai-explanation.fast-blink:after
    animation blink-underline-fast 0.4s ease-in-out infinite

#post
  border var(--style-border)
  border-radius var(--radius)
  box-shadow var(--efu-shadow-border)
  background var(--efu-card-bg)

添加加载延迟提示

修改pug文件

路径:
solitude/layout/includes/widgets/post/ai.pug

\\ 修改第7行
    .ai-explanation(style="display: block;")=theme.post.ai.loading

修改配置文件

post:
  # 其他配置内容
  ai:
    # 其他配置内容
    loading: 拼命加载中··· # 添加延迟加载期间的提示文字

图标修改

路径:
solitude/layout/includes/widgets/home/bbTimeList.pug

第2行

i.bber-logo.solitude.far.fa-newspaper.fa-bounce(onclick=`pjax.loadUrl('${url_for(theme.brevity.page)}')`)

路径:
solitude/layout/includes/widgets/nav/right.pug

第9行

i.solitude.iconfont.icon-dice-line

取消《开往》按钮移动端自动消失

路径:
solitude/source/css/_layout/header.styl

\\ 删除272-274行
    #travellings_button
      +maxWidth768()
        opacity 0