4.主题魔改:侧边栏添加“最新评论”模块

4.主题魔改:侧边栏添加“最新评论”模块

重要前提:评论后端不匹配问题

提供的这份教程是为 Twikoo 评论系统设计的。


核心流程概览
  1. 创建功能核心JS文件:一份JS文件将包含所有的样式和逻辑。
  2. 在侧边栏添加小组件配置:告诉主题要在侧边栏的哪个位置显示“最新评论”这个卡片。
  3. 通过主题配置注入JS文件:让网站加载我们创建的功能脚本。

第一步:创建核心功能文件 (comments.js)
  1. 创建JS文件

    • 在您主题的 source/js/ 目录下,新建一个文件,命名为 comments.js
    • 文件路径示例themes/anzhiyu/source/js/comments.js
  2. 粘贴JS代码

    • 将下面的JavaScript代码完整复制到您刚创建的 comments.js 文件中。
    1
    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
    (() => {
    const injectCSS = () => {
    const style = document.createElement('style');
    style.textContent = `
    #aside-content .aside-list > .aside-list-item .content{
    width: 3.2em !important;
    height: 3.2em !important;
    display: flex;
    flex-direction: column;
    justify-content: space-around;
    }
    #aside-content .aside-list > .aside-list-item .thumbnail {
    width: 3.2em!important;
    height: 3.2em!important;
    }

    .card-latest-comments .item-headline i {
    color: var(--anzhiyu-main);
    }

    .card-latest-comments .headline-right {
    position: absolute;
    right: 24px;
    top: 16px;
    transition: all 0.3s;
    opacity: 0.6;
    }

    .card-latest-comments .headline-right:hover {
    color: var(--anzhiyu-main);
    opacity: 1;
    transform: rotate(90deg);
    }

    .aside-list-author {
    display: flex;
    align-items: center;
    font-weight: bold;
    height: 22px;
    gap: 5px;
    }

    .aside-list-date {
    font-size: 0.7rem;
    font-weight: normal;
    margin-left: auto;
    }

    .aside-list-content {
    font-size: 0.9rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    text-decoration: none;
    line-height: 1.2;
    }

    .aside-list-item:last-child {
    margin-bottom: 0!important;
    }

    [data-theme='dark'] .aside-list-item-right {
    filter: brightness(0.95);
    }

    .aside-list-author-name {
    display: flex;
    align-items: center;
    white-space: nowrap;
    gap: 4px;
    max-width: 65%;
    }

    .aside-list-author-name span {
    overflow: hidden;
    text-overflow: ellipsis;
    }

    .aside-list-author-name svg {
    flex-shrink: 0;
    }
    `;
    document.head.appendChild(style);
    };

    const LatestComments = {
    API_URL: 'https://twikoo.ruom.top',
    ADMIN_EMAIL_MD5: 'f2c9c64c90a00afeed5ba410e5447a0d01aa294874bd662032a27c5385bcde1c',
    PAGE_SIZE: 5,
    LOADING_GIF: 'https://lib.bsgun.cn/Hexo-static/img/loading.gif',

    async fetchComments() {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 5000);

    try {
    const response = await fetch(this.API_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
    event: 'GET_RECENT_COMMENTS',
    includeReply: true,
    pageSize: this.PAGE_SIZE
    }),
    signal: controller.signal
    });

    const { data } = await response.json();
    return data;
    } catch (error) {
    console.error('获取评论出错:', error);
    return null;
    } finally {
    clearTimeout(timeoutId);
    }
    },

    formatTimeAgo(timestamp) {
    const diff = Math.floor((Date.now() - new Date(timestamp)) / 1000);
    if (diff < 60) return '刚刚';
    if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`;
    if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`;
    if (diff < 604800) return `${Math.floor(diff / 86400)}天前`;

    return new Date(timestamp).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' }) + '日';
    },

    formatContent(content) {
    if (!content) return '';

    return content
    .replace(/<pre><code>[\s\S]*?<\/code><\/pre>/g, '[代码块]')
    .replace(/<code>([^<]{4,})<\/code>/g, '[代码]')
    .replace(/<code>([^<]{1,3})<\/code>/g, '$1')
    .replace(/<img[^>]*>/g, '[图片]')
    .replace(/<a[^>]*?>[\s\S]*?<\/a>/g, '[链接]')
    .replace(/<[^>]+>/g, '')
    .replace(/&(gt|lt|amp|quot|#39|nbsp);/g, m =>
    ({'>':'>', '<':'<', '&':'&', 'quot':'"', '#39':"'", 'nbsp':' '})[m.slice(1,-1)])
    .replace(/\s+/g, ' ')
    .trim();
    },

    generateCommentHTML(comment) {
    const { created, comment: content, url, avatar, nick, mailMd5, id } = comment;
    const timeAgo = this.formatTimeAgo(created);
    const formattedContent = this.formatContent(content);
    const adminBadge = mailMd5 === this.ADMIN_EMAIL_MD5 ? `
    <svg t="1731283534336" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="29337" width="22" height="22"><path d="M512 0C230.4 0 0 230.4 0 512s230.4 512 512 512 512-230.4 512-512S793.6 0 512 0z m291.84 366.08c-46.08 0-79.36 23.04-92.16 66.56l-163.84 358.4h-66.56L312.32 435.2c-17.92-46.08-46.08-71.68-89.6-71.68v-35.84H512v35.84h-40.96c-25.6 2.56-30.72 23.04-12.8 61.44l102.4 225.28 89.6-199.68c25.6-56.32 2.56-84.48-71.68-89.6v-35.84h225.28v40.96z" fill="#06c013" p-id="29338" data-spm-anchor-id="a313x.search_index.0.i73.2b2d3a81BgxnVW" class=""></path></svg>` : '';

    return `
    <div class="aside-list-item" title="${formattedContent}" onclick="pjax.loadUrl('${url}#${id}')">
    <div class="thumbnail">
    <img class="aside-list-avatar" src="${avatar}" alt="avatar">
    </div>
    <div class="content">
    <div class="aside-list-author">
    <div class="aside-list-author-name">
    <span>${nick}</span>${adminBadge}
    </div>
    <span class="aside-list-date">${timeAgo}</span>
    </div>
    <div class="aside-list-content">${formattedContent}</div>
    </div>
    </div>
    `;
    },

    getErrorTemplate(icon, message) {
    return `
    <div style="min-height: 346px;display: flex;padding: 20px;text-align: center;justify-content: center;align-items: center;flex-direction: column;">
    <i class="fas fa-${icon}" style="font-size: 2rem; color: ${icon === 'exclamation-circle' ? '#ff6b6b' : '#999'}; margin-bottom: 10px;"></i>
    <p style="color: #666;margin: 0;">${message}</p>
    </div>
    `;
    },

    async insertComponent() {
    const container = document.getElementById("latest-comments");
    if (!container) return;

    container.innerHTML = `<img src="${this.LOADING_GIF}" style="display: flex;min-height: 346px;object-fit: cover;">`;

    const comments = await this.fetchComments();
    let content;

    if (comments === null) {
    content = this.getErrorTemplate('exclamation-circle', '评论加载失败,请稍后再试');
    } else if (comments.length === 0) {
    content = this.getErrorTemplate('comment-slash', '还没有评论呢~ 快来抢沙发吧!');
    } else {
    content = comments.map(this.generateCommentHTML.bind(this)).join('');
    }

    container.style.opacity = '0';
    container.innerHTML = content;

    requestAnimationFrame(() => {
    container.style.transition = 'opacity 0.3s ease-in';
    container.style.opacity = '1';
    });
    }
    };

    // 初始化时注入CSS并启动组件
    ['DOMContentLoaded', 'pjax:success'].forEach(event =>
    document.addEventListener(event, () => {
    injectCSS();
    LatestComments.insertComponent();
    })
    );
    })();

第二步:在侧边栏中添加小组件
  1. 找到或创建 widget.yml

    • 这个文件控制了侧边栏显示哪些小组件以及它们的顺序。
    • 文件路径:source/_data/widget.yml。如果 _datawidget.yml 不存在,请手动创建。
  2. 添加“最新评论”卡片配置

    • widget.yml 文件中添加以下内容。您可以把它放在 top: 列表的任意位置,来决定它在侧边栏顶部区域的显示顺序。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    top:
    - class_name: card-latest-comments
    name: 最新评论
    icon: fas fa-comments
    html: |
    <a href="/messages/" class="headline-right" title="查看更多">
    <i class="fas fa-angle-right"></i>
    </a>
    <div class="aside-list" id="latest-comments"></div>

第三步:在主题配置中注入JS文件
  1. 打开主题配置文件 (themes/anzhiyu/_config.yml)。
  2. 找到 inject: 配置项,在 bottom: 列表中添加我们新建的JS文件。
    1
    2
    3
    4
    inject:
    bottom:
    # - 其他 bottom 内容
    - '<script src="/js/comments.js"></script>'

第四步:个性化配置说明

您需要修改 comments.js 文件顶部的几个关键参数,使其与您自己的Twikoo服务匹配。

  • API_URL: 【必填】'https://twikoo.ruom.top' 替换为您自己部署的Twikoo后端服务地址。

  • ADMIN_EMAIL_MD5: 【必填】 替换为您自己作为博主的邮箱的MD5值。这用于在评论列表中为您显示“博主”标识。您可以右键点击评论的头像选择在新窗口打开截取一下的字段

    1
    2
    3
    https://weavatar.com/avatar/ec3291d59a8d7d3675df8a0537fcbc979f0fa611c8df55841fb7f4fd162bd07a?d=initials&name=Prorise

    ec3291d59a8d7d3675df8a0537fcbc979f0fa611c8df55841fb7f4fd162bd07a <- 即为MD5值
  • PAGE_SIZE: 显示的最新评论数量,您可以按需修改。

  • LOADING_GIF: 加载时显示的动画图片,您可以替换为您喜欢的图片链接。