diff --git a/_config.yml b/_config.yml
index 179e73078..4889322ee 100644
--- a/_config.yml
+++ b/_config.yml
@@ -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
@@ -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"
# 国内大陆服务器的备案信息
diff --git a/layout/_partials/footer/statistics.ejs b/layout/_partials/footer/statistics.ejs
index 9f05e2dc8..f129adad7 100644
--- a/layout/_partials/footer/statistics.ejs
+++ b/layout/_partials/footer/statistics.ejs
@@ -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) { %>
+
+ <%- pv_texts[0] %>
+
+ <%- pv_texts[1] %>
+
+ <% } %>
+ <% if (uv_texts.length >= 2) { %>
+
+ <%- uv_texts[0] %>
+
+ <%- uv_texts[1] %>
+
+ <% } %>
+ <% import_js(theme.static_prefix.internal_js, 'openkounter.js', 'defer') %>
+
<% } else if (theme.footer.statistics.source === 'busuanzi') { %>
<% if (pv_texts.length >= 2) { %>
diff --git a/layout/_partials/plugins/analytics.ejs b/layout/_partials/plugins/analytics.ejs
index 8bdd0a2d8..b5ebb1b87 100644
--- a/layout/_partials/plugins/analytics.ejs
+++ b/layout/_partials/plugins/analytics.ejs
@@ -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') %>
+ <% } %>
+
<% } %>
diff --git a/layout/_partials/post/meta-top.ejs b/layout/_partials/post/meta-top.ejs
index 3e2e54525..3b15c4d8e 100644
--- a/layout/_partials/post/meta-top.ejs
+++ b/layout/_partials/post/meta-top.ejs
@@ -51,7 +51,13 @@
<%- views_texts[0] %><%- views_texts[1] %>
<% import_js(theme.static_prefix.internal_js, 'leancloud.js', 'defer') %>
-
+ <% } else if (theme.post.meta.views.source === 'openkounter') { %>
+
+
+ <%- views_texts[0] %><%- views_texts[1] %>
+
+ <% import_js(theme.static_prefix.internal_js, 'openkounter.js', 'defer') %>
+
<% } else if (theme.post.meta.views.source === 'busuanzi') { %>
diff --git a/source/js/openkounter.js b/source/js/openkounter.js
new file mode 100644
index 000000000..97ce54829
--- /dev/null
+++ b/source/js/openkounter.js
@@ -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);
+