icarus主题优化

icarus主题复制功能重构(仿照next主题)、firebase统计阅读人数

复制功能重构

  • icarus主题自带的复制功能是带文字选中的(如下图),个人感觉麻烦了点,于是考虑借鉴next的复制风格,一键复制

    官网指南截图

  • 实现:

    hexo-theme-icarus/source/js/main.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
            if (clipboard) {
    - new ClipboardJS('.highlight .copy', {
    - target: function(trigger) {
    - return trigger.parentNode.nextElementSibling;
    - }
    - }).on('success', function(e) {
    - e.clearSelection();
    - const tmp = e.trigger.innerHTML;
    - e.trigger.innerHTML = '<i class="fas fa-check"></i>';
    - setTimeout(function() {
    - e.trigger.innerHTML = tmp;
    - }, 2000);
    - });
    + $('figure.highlight').each(function() {
    + const target = $(this).find('figcaption div.level-right')[0];
    + if (target) {
    + registerCopyButton(target, this);
    + }
    + });
    }

    ----------------------------------------------------------------------------------

    ...

    $('.article > .content > table').each(function() {
    if ($(this).width() > $(this).parent().width()) {
    $(this).wrap('<div class="table-overflow"></div>');
    }
    });

    + // 注册复制按钮功能,仿照Next主题实现
    + function registerCopyButton(target, element) {
    + // 添加复制按钮
    + target.insertAdjacentHTML('beforeend', '<a href="javascript:;" class="copy" title="Copy"><i class="fas fa-copy"></i></a>');
    + const button = target.querySelector('.copy');
    +
    + button.addEventListener('click', function() {
    + const code = element.querySelector('.code').innerText;
    +
    + if (navigator.clipboard) {
    + // 使用现代的Clipboard API
    + navigator.clipboard.writeText(code).then(function() {
    + button.querySelector('i').className = 'fas fa-check-circle';
    + }, function() {
    + button.querySelector('i').className = 'fas fa-times-circle';
    + });
    + } else {
    + // 兼容性回退方案
    + const ta = document.createElement('textarea');
    + ta.style.top = window.scrollY + 'px';
    + ta.style.position = 'absolute';
    + ta.style.opacity = '0';
    + ta.readOnly = true;
    + ta.value = code;
    + document.body.append(ta);
    + ta.select();
    + ta.setSelectionRange(0, code.length);
    + ta.readOnly = false;
    + const result = document.execCommand('copy');
    + button.querySelector('i').className = result ? 'fas fa-check-circle' : 'fas fa-times-circle';
    + ta.blur();
    + button.blur();
    + document.body.removeChild(ta);
    + }
    + });
    +
    + // 鼠标离开300ms后恢复复制图标
    + element.addEventListener('mouseleave', function() {
    + setTimeout(function() {
    + const icon = button.querySelector('i');
    + if (icon) {
    + icon.className = 'fas fa-copy';
    + }
    + }, 300);
    + });
    + }

    function adjustNavbar() {
    const navbarWidth = $('.navbar-main .navbar-start').outerWidth() + $('.navbar-main .navbar-end').outerWidth();
    if ($(document).outerWidth() < navbarWidth) {
    $('.navbar-main .navbar-menu').addClass('justify-content-start');
    } else {
    $('.navbar-main .navbar-menu').removeClass('justify-content-start');
    }
    }

    hexo-theme-icarus/layout/common/scripts.jsx

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
               return <Fragment>
    <script src={cdn('jquery', '3.3.1', 'dist/jquery.min.js')}></script>
    <script src={cdn('moment', '2.22.2', 'min/moment-with-locales.min.js')}></script>
    <script dangerouslySetInnerHTML={{ __html: `moment.locale("${language}");` }}></script>
    <script dangerouslySetInnerHTML={{ __html: embeddedConfig }}></script>
    <script data-pjax src={url_for('/js/column.js')}></script>
    <Plugins site={site} config={config} page={page} helper={helper} head={false} />
    - {clipboard && <script src={cdn('clipboard', '2.0.4', 'dist/clipboard.min.js')} defer></script>}
    <script data-pjax src={url_for('/js/main.js')} defer></script>
    </Fragment>;

使用firebase统计阅读人数

config文件添加配置

_config.icarus.yml

1
2
3
4
5
6
7
+# Firebase Firestore 阅读人数统计
+services:
+ firebase:
+ enable: true
+ collection: articles # Firestore数据库中的集合名称
+ apiKey: # 你的Firebase API Key
+ projectId: # 你的Firebase项目ID

文章head增加人数统计

hexo-theme-icarus/layout/common/articles.jsx

1
2
3
4
5
6
7
8
9
                            {/* Visitor counter */}                            
{!index && plugins && plugins.busuanzi === true ? <span class="level-item" id="busuanzi_container_page_pv" dangerouslySetInnerHTML={{
__html: _p('plugin.visit_count', '<span id="busuanzi_value_page_pv">0</span>')
}}></span> : null}
+ {/* Firebase Visitor counter */}
+ {config.services && config.services.firebase && config.services.firebase.enable ? <span class="level-item">
+ <i class="far fa-eye mr-1"></i>
+ <span class="firestore-visitors-count">0</span>
+ </span> : null}

基于firebase的统计功能实现

source/js/firebase_counter.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
+/* global IcarusThemeSettings, firebase */
+
+// Firebase Counter - 阅读量统计功能
+
+if (IcarusThemeSettings && IcarusThemeSettings.services && IcarusThemeSettings.services.firebase && IcarusThemeSettings.services.firebase.enable) {
+
+ // 初始化Firebase
+ try {
+ // 确保firebase对象存在
+ if (typeof firebase === 'undefined') {
+ console.error('Firebase library not loaded');
+ // 如果Firebase库未加载,隐藏计数器元素
+ document.querySelectorAll('.firestore-visitors-count').forEach(el => {
+ el.style.display = 'none';
+ });
+ } else {
+
+ firebase.initializeApp({
+ apiKey: IcarusThemeSettings.services.firebase.apiKey,
+ projectId: IcarusThemeSettings.services.firebase.projectId
+ });
+
+ // 开发环境检测
+ const isLocalhost = ['localhost', '127.0.0.1'].includes(window.location.hostname);
+ const isDevelopment = isLocalhost || window.location.hostname.includes('192.168.');
+
+ // 移除所有调试日志输出
+
+ // 获取数据库引用
+ const db = firebase.firestore();
+ const articlesCollection = IcarusThemeSettings.services.firebase.collection || 'articles';
+ const articles = db.collection(articlesCollection);
+
+ // 获取阅读次数函数
+ const getCount = (doc, increaseCount) => {
+ // 获取文档数据
+ return doc.get().then(d => {
+ // 初始化计数
+ let count = d.exists ? d.data().count : 0;
+
+ // 如果需要增加计数(只在文章页面且未在同一会话访问过)
+ if (increaseCount) {
+ // 增加计数
+ count++;
+ return doc.set({ count }).then(() => {
+ if (isDevelopment) console.log('计数已更新:', count);
+ return count;
+ }).catch(error => {
+ console.error('Error updating count:', error);
+ return count; // 返回增加前的计数
+ });
+ }
+
+ return count;
+ }).catch(error => {
+ console.error('Error getting count:', error);
+ // 出错时返回默认值0
+ return 0;
+ });
+ };
+
+ // 处理阅读计数的函数
+ const handleViewCount = () => {
+ // 获取当前URL路径
+ const currentPath = window.location.pathname;
+
+ // 增强首页识别:使用多个可能的选择器
+ const isIndexPage = document.querySelector('.article-list') !== null ||
+ document.querySelector('.article-card-list') !== null ||
+ document.querySelectorAll('.article-card').length > 0 || // 降低阈值以适应首页
+ (currentPath === '/' && document.querySelector('article.article') !== null); // 特殊处理:根路径+article.article元素
+
+ // 增强文章页面识别:使用更精确的选择器组合
+ // 重要:首页优先判断,且文章页判断要排除首页的情况,并增加特定的文章页特征
+ const isArticlePage = !isIndexPage && (
+ // 传统文章页选择器
+ (document.querySelector('.article-container') !== null && document.querySelector('.article-content') !== null) ||
+ (document.querySelector('article.post') !== null && document.querySelector('.article-content') !== null) ||
+ document.querySelector('[id="post-content"]') !== null ||
+ // 针对hexo s环境的增强选择器,但需要确保不是首页
+ (document.querySelector('article.article') !== null &&
+ document.querySelector('article.card-content.article') !== null &&
+ document.querySelectorAll('.article-card').length === 0 &&
+ currentPath !== '/')
+ );
+
+ // 页面类型信息 - 仅在开发环境显示
+ if (isDevelopment) {
+ console.log(`[Firebase] 当前页面类型: ${isArticlePage ? '文章页' : isIndexPage ? '首页' : '其他页面'}`);
+ }
+
+ if (isArticlePage) {
+ // 文章页面处理
+ const titleElement = document.querySelector('.title.is-3, .title.is-4-mobile');
+ const countElement = document.querySelector('.firestore-visitors-count');
+
+ if (titleElement && countElement) {
+ const title = titleElement.textContent.trim();
+ const doc = articles.doc(title);
+
+ // 确定是否增加计数:不在本地开发环境且同一会话未访问过
+ let increaseCount = !isDevelopment;
+
+ if (sessionStorage.getItem(title)) {
+ increaseCount = false;
+ } else {
+ // 标记为在当前会话中已访问
+ sessionStorage.setItem(title, true);
+ }
+
+ getCount(doc, increaseCount).then(count => {
+ countElement.innerText = count;
+ }).catch(e => {
+ // 移除错误日志
+ });
+ } else {
+ // 静默处理:未找到文章标题或计数元素
+ }
+ } else if (isIndexPage) {
+ // 首页文章列表处理
+ // 修改选择器以匹配实际HTML结构
+ // 首页文章结构: .card > .card-content.article > .title.is-3.is-size-4-mobile
+ const titleElements = document.querySelectorAll(
+ '.card .article .title.is-3, .card .article .title.is-4-mobile, ' +
+ '.card article .title.is-3, .card article .title.is-4-mobile, ' +
+ '.card .article p.title, .card article p.title'
+ );
+ const countElements = document.querySelectorAll('.card .article .firestore-visitors-count, .card article .firestore-visitors-count');
+
+ // 移除首页元素检测日志
+
+ if (titleElements.length > 0 && countElements.length > 0) {
+ const promises = [...titleElements].map(element => {
+ const title = element.textContent.trim();
+ const doc = articles.doc(title);
+ // 首页只获取计数,不增加计数
+ return getCount(doc, false).then(count => {
+ return count;
+ });
+ });
+
+ Promise.all(promises).then(counts => {
+ counts.forEach((val, idx) => {
+ if (countElements[idx]) {
+ countElements[idx].innerText = val;
+ }
+ });
+ }).catch(e => {
+ // 错误处理:尝试显示一些默认值或替代文本
+ countElements.forEach(el => {
+ if (el.innerText === '0') {
+ el.innerText = '加载中...';
+ }
+ });
+ });
+ } else {
+ // 尝试使用更通用的选择器
+ const fallbackTitleElements = document.querySelectorAll('.article-card a[href^="/"]');
+ const fallbackCountElements = document.querySelectorAll('.article-card .firestore-visitors-count');
+
+ if (fallbackTitleElements.length > 0 && fallbackCountElements.length > 0) {
+ // 这里可以添加备选逻辑
+ }
+ }
+ } else {
+ // 静默处理:当前页面既不是文章页也不是首页
+ }
+ };
++
+ // 监听传统页面加载完成事件
+ document.addEventListener('DOMContentLoaded', () => {
+ handleViewCount();
+ });
+
+ // 监听PJAX页面加载完成事件(适配Icarus主题的PJAX机制)
+ document.addEventListener('page:loaded', () => {
+ handleViewCount();
+ });
+ }
+ } catch (error) {
+ // 移除初始化失败日志
+ // 如果初始化失败,尝试隐藏计数器元素以避免显示为0
+ document.querySelectorAll('.firestore-visitors-count').forEach(el => {
+ el.style.display = 'none';
+ });
+ }
+}

其他一些优化

hexo-theme-icarus/layout/common/scripts.jsx

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
const { Component, Fragment } = require('inferno');
const { toMomentLocale } = require('hexo/dist/plugins/helper/date');
const Plugins = require('./plugins');

module.exports = class extends Component {
render() {
const { site, config, helper, page } = this.props;
const { url_for, cdn } = helper;
const { article } = config;
const language = toMomentLocale(page.lang || page.language || config.language || 'en');

let fold = 'unfolded';
let clipboard = true;
if (article && article.highlight) {
if (typeof article.highlight.clipboard !== 'undefined') {
clipboard = !!article.highlight.clipboard;
}
if (typeof article.highlight.fold === 'string') {
fold = article.highlight.fold;
}
}

const embeddedConfig = `var IcarusThemeSettings = {
article: {
highlight: {
clipboard: ${clipboard},
fold: '${fold}'
}
+ },
+ services: {
+ firebase: ${config.services && config.services.firebase ? JSON.stringify(config.services.firebase) : 'false'}
}
};`;

return <Fragment>
<script src={cdn('jquery', '3.3.1', 'dist/jquery.min.js')}></script>
<script src={cdn('moment', '2.22.2', 'min/moment-with-locales.min.js')}></script>
<script dangerouslySetInnerHTML={{ __html: `moment.locale("${language}");` }}></script>
<script dangerouslySetInnerHTML={{ __html: embeddedConfig }}></script>
+ {/* Firebase Firestore */}
+ {config.services && config.services.firebase && config.services.firebase.enable ? (
+ <Fragment>
+ <script src="https://www.gstatic.com/firebasejs/9.6.11/firebase-app-compat.js"></script>
+ <script src="https://www.gstatic.com/firebasejs/9.6.11/firebase-firestore-compat.js"></script>
+ </Fragment>
+ ) : null}
<script data-pjax src={url_for('/js/column.js')}></script>
<Plugins site={site} config={config} page={page} helper={helper} head={false} />
<script data-pjax src={url_for('/js/main.js')} defer></script>
+ {/* Firebase counter script */}
+ {config.services && config.services.firebase && config.services.firebase.enable ? (
+ <script data-pjax src={url_for('/js/firebase_counter.js')} defer></script>
+ ) : null}
</Fragment>;
}
};
作者

SydzI

发布于

2025-09-02

更新于

2025-09-07

许可协议

评论