Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions _config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,19 @@ web_analytics:
# If true, ignore localhost & 127.0.0.1
ignore_local: false

# OpenKounter 计数统计(基于 EdgeOne/Cloudflare Workers),可用于 PV UV 展示
# OpenKounter count statistics (based on EdgeOne/Cloudflare Workers), which can be used for PV UV display
openkounter:
# OpenKounter API 服务器地址
# OpenKounter API server URL
server_url: https://open-kounter.mintimate.cn
# 统计页面时获取路径的属性,通常使用 window.location.pathname
# Get the attribute of the page path during statistics
path: window.location.pathname
# 开启后不统计本地路径(localhost、127.0.0.1、[::1])
# If true, ignore localhost & 127.0.0.1 & [::1]
ignore_local: false

# Umami Analytics,仅支持自部署。如果要展示 PV UV 需要填写所有配置项,否则只填写 `src` 和 `website_id` 即可
# Umami Analytics, only Self-host support. If you want to display PV UV need to set all config items, otherwise only set 'src' and 'website_id'
# See: https://umami.is/docs
Expand Down Expand Up @@ -456,9 +469,9 @@ footer:
statistics:
enable: false

# 统计数据来源,使用 leancloud, umami 需要设置 `web_analytics` 中对应的参数;使用 busuanzi 不需要额外设置,但是有时不稳定,另外本地运行时 busuanzi 显示统计数据很大属于正常现象,部署后会正常
# Data source. If use leancloud, umami, you need to set the parameter in `web_analytics`
# Options: busuanzi | leancloud | umami
# 统计数据来源,使用 leancloud, umami, openkounter 需要设置 `web_analytics` 中对应的参数;使用 busuanzi 不需要额外设置,但是有时不稳定,另外本地运行时 busuanzi 显示统计数据很大属于正常现象,部署后会正常
# Data source. If use leancloud, umami, openkounter, you need to set the parameter in `web_analytics`
# Options: busuanzi | leancloud | umami | openkounter
source: "busuanzi"

# 国内大陆服务器的备案信息
Expand Down
17 changes: 17 additions & 0 deletions layout/_partials/footer/statistics.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,23 @@
<% } %>
<% import_js(theme.static_prefix.internal_js, 'leancloud.js', 'defer') %>

<% } else if (theme.footer.statistics.source === 'openkounter') { %>
<% if (pv_texts.length >= 2) { %>
<span id="openkounter-site-pv-container" style="display: none">
<%- pv_texts[0] %>
<span id="openkounter-site-pv"></span>
<%- pv_texts[1] %>
</span>
<% } %>
<% if (uv_texts.length >= 2) { %>
<span id="openkounter-site-uv-container" style="display: none">
<%- uv_texts[0] %>
<span id="openkounter-site-uv"></span>
<%- uv_texts[1] %>
</span>
<% } %>
<% import_js(theme.static_prefix.internal_js, 'openkounter.js', 'defer') %>

<% } else if (theme.footer.statistics.source === 'busuanzi') { %>
<% if (pv_texts.length >= 2) { %>
<span id="busuanzi_container_site_pv" style="display: none">
Expand Down
5 changes: 5 additions & 0 deletions layout/_partials/plugins/analytics.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,9 @@
<% if(theme.web_analytics.leancloud && theme.web_analytics.leancloud.app_id && theme.web_analytics.leancloud.app_key) { %>
<% import_js(theme.static_prefix.internal_js, 'leancloud.js', 'defer') %>
<% } %>

<% if(theme.web_analytics.openkounter && theme.web_analytics.openkounter.server_url) { %>
<% import_js(theme.static_prefix.internal_js, 'openkounter.js', 'defer') %>
<% } %>

<% } %>
8 changes: 7 additions & 1 deletion layout/_partials/post/meta-top.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,13 @@
<%- views_texts[0] %><span id="leancloud-page-views"></span><%- views_texts[1] %>
</span>
<% import_js(theme.static_prefix.internal_js, 'leancloud.js', 'defer') %>

<% } else if (theme.post.meta.views.source === 'openkounter') { %>
<span id="openkounter-page-views-container" style="display: none">
<i class="iconfont icon-eye" aria-hidden="true"></i>
<%- views_texts[0] %><span id="openkounter-page-views"></span><%- views_texts[1] %>
</span>
<% import_js(theme.static_prefix.internal_js, 'openkounter.js', 'defer') %>

<% } else if (theme.post.meta.views.source === 'busuanzi') { %>
<span id="busuanzi_container_page_pv" style="display: none">
<i class="iconfont icon-eye" aria-hidden="true"></i>
Expand Down
173 changes: 173 additions & 0 deletions source/js/openkounter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/* global CONFIG, Fluid */

(function(window, document) {
'use strict';

// Get server URL from config
const API_SERVER = (CONFIG.web_analytics.openkounter && CONFIG.web_analytics.openkounter.server_url) || '';

if (!API_SERVER) {
console.warn('OpenKounter: server_url is not configured');
return;
}

function getRecord(target) {
return fetch(`${API_SERVER}/api/counter?target=${encodeURIComponent(target)}`)
.then(resp => {
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
}
return resp.json();
})
.then(({ data, code, message }) => {
if (code !== 0) {
throw new Error(message || 'Unknown error');
}
return { time: data.time || 0, objectId: data.target };
})
.catch(error => {
console.error('OpenKounter fetch error:', error);
return { time: 0, objectId: target };
});
}

function increment(incrArr) {
if (!incrArr || incrArr.length === 0) {
return Promise.resolve([]);
}

return fetch(`${API_SERVER}/api/counter`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'batch_inc',
requests: incrArr
})
})
.then(res => {
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
return res.json();
})
.then(res => {
if (res.code !== 0) {
throw new Error(res.message || 'Failed to increment counter');
}
return res.data;
})
.catch(error => {
console.error('OpenKounter increment error:', error);
});
}

function buildIncrement(objectId) {
return { target: objectId };
}

// 校验是否为有效的主机(排除本地开发环境)
function validHost() {
const ignoreLocal = CONFIG.web_analytics.openkounter && CONFIG.web_analytics.openkounter.ignore_local;
if (ignoreLocal !== false) {
const hostname = window.location.hostname;
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]') {
return false;
}
}
return true;
}

// 校验是否为有效的独立访客(24小时内只计一次)
function validUV() {
const key = 'OpenKounter_UV_Flag';
const now = Date.now();

try {
const flag = localStorage.getItem(key);
if (flag) {
const lastVisit = parseInt(flag, 10);
// 距离上次访问小于 24 小时则不计为新 UV
if (now - lastVisit <= 86400000) {
return false;
}
}
localStorage.setItem(key, now.toString());
} catch (e) {
// localStorage 不可用时默认计为 UV
console.warn('OpenKounter: localStorage is not available');
}
return true;
}

function addCount() {
const enableIncr = CONFIG.web_analytics.enable && (!window.Fluid || !Fluid.ctx.dnt) && validHost();
const getterArr = [];
const incrArr = [];

// 请求站点 PV 并自增
const pvCtn = document.querySelector('#openkounter-site-pv-container');
if (pvCtn) {
const pvGetter = getRecord('site-pv').then((record) => {
if (enableIncr) {
incrArr.push(buildIncrement(record.objectId));
}
const ele = document.querySelector('#openkounter-site-pv');
if (ele) {
ele.innerText = (record.time || 0) + (enableIncr ? 1 : 0);
pvCtn.style.display = 'inline';
}
});
getterArr.push(pvGetter);
}

// 请求站点 UV 并自增
const uvCtn = document.querySelector('#openkounter-site-uv-container');
if (uvCtn) {
const uvGetter = getRecord('site-uv').then((record) => {
const incrUV = validUV() && enableIncr;
if (incrUV) {
incrArr.push(buildIncrement(record.objectId));
}
const ele = document.querySelector('#openkounter-site-uv');
if (ele) {
ele.innerText = (record.time || 0) + (incrUV ? 1 : 0);
uvCtn.style.display = 'inline';
}
});
getterArr.push(uvGetter);
}

// 请求页面浏览数并自增
const viewCtn = document.querySelector('#openkounter-page-views-container');
if (viewCtn) {
const pathConfig = CONFIG.web_analytics.openkounter.path || 'window.location.pathname';
const path = eval(pathConfig);
const target = decodeURI(path.replace(/\/*(index.html)?$/, '/'));

const viewGetter = getRecord(target).then((record) => {
if (enableIncr) {
incrArr.push(buildIncrement(record.objectId));
}
const ele = document.querySelector('#openkounter-page-views');
if (ele) {
ele.innerText = (record.time || 0) + (enableIncr ? 1 : 0);
viewCtn.style.display = 'inline';
}
});
getterArr.push(viewGetter);
}

// 批量发起统计请求
Promise.all(getterArr).then(() => {
if (enableIncr && incrArr.length > 0) {
increment(incrArr);
}
}).catch(error => {
console.error('OpenKounter error:', error);
});
}

addCount();

})(window, document);

Loading