Skip to content
Closed
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
829 changes: 29 additions & 800 deletions src/main/resources/static/js/dataStatistics.js

Large diffs are not rendered by default.

98 changes: 98 additions & 0 deletions src/main/resources/static/js/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
(function() {
'use strict';

const { detectEmbedMode } = window.xhhaocomDataStatisticsV2Utils || {};
const { initTrafficStats } = window.xhhaocomDataStatisticsV2Traffic || {};
const { initRealtimeActivity } = window.xhhaocomDataStatisticsV2Activity || {};
const { initUptimeKumaStatus } = window.xhhaocomDataStatisticsV2Uptime || {};
const {
initGithubPin,
initGithubStats,
initGithubTopLangs,
initGithubGraph,
initGithubStatisticsContainer
} = window.xhhaocomDataStatisticsV2Github || {};

const COMPONENT_INIT_MAP = {
'traffic': initTrafficStats,
'activity': initRealtimeActivity,
'uptime-kuma': initUptimeKumaStatus,
'github-pin': initGithubPin,
'github-stats': initGithubStats,
'github-top-langs': initGithubTopLangs,
'github-graph': initGithubGraph
};

const COMPONENT_SELECTORS = [
'.xhhaocom-dataStatistics-v2-traffic',
'.xhhaocom-dataStatistics-v2-activity',
'.xhhaocom-dataStatistics-v2-uptime-kuma',
'.xhhaocom-dataStatistics-v2-github-pin',
'.xhhaocom-dataStatistics-v2-github-stats',
'.xhhaocom-dataStatistics-v2-github-top-langs',
'.xhhaocom-dataStatistics-v2-github-graph'
];

function detectComponentType(className) {
if (className.includes('traffic')) return 'traffic';
if (className.includes('activity')) return 'activity';
if (className.includes('uptime-kuma')) return 'uptime-kuma';
if (className.includes('github-pin')) return 'github-pin';
if (className.includes('github-stats')) return 'github-stats';
if (className.includes('github-top-langs')) return 'github-top-langs';
if (className.includes('github-graph')) return 'github-graph';
return null;
}

function initComponent(element, componentType) {
const initFn = COMPONENT_INIT_MAP[componentType];
if (!initFn) return;

if (componentType === 'traffic' || componentType === 'activity') {
const embedMode = detectEmbedMode ? detectEmbedMode(element) : { isEmbed: false, isArticle: false, isSidebar: false };
initFn(element, embedMode);
} else {
initFn(element);
}
}

function init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
return;
}

if (initGithubStatisticsContainer) {
document.querySelectorAll('.github-statistics-container').forEach(container => {
initGithubStatisticsContainer(container);
});
}

COMPONENT_SELECTORS.forEach(selector => {
document.querySelectorAll(selector).forEach(element => {
if (element.hasAttribute('data-initialized')) {
return;
}

const componentType = detectComponentType(element.className);
if (componentType) {
element.setAttribute('data-initialized', 'true');
initComponent(element, componentType);
}
});
});
}

window.xhhaocomDataStatisticsV2Init = init;
init();

if (typeof MutationObserver !== 'undefined') {
const observer = new MutationObserver(() => {
init();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
})();
119 changes: 119 additions & 0 deletions src/main/resources/static/js/modules/dataStatistics/activity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
(function() {
'use strict';

const { showLoading, safeFetch } = window.xhhaocomDataStatisticsV2Utils || {};
const { MAX_ACTIVITY_EVENTS } = window.xhhaocomDataStatisticsV2Constants || {};
const { createActivityMetric } = window.xhhaocomDataStatisticsV2DOM || {};
const { formatTimeChinese, formatDeviceInfo } = window.xhhaocomDataStatisticsV2Formatters || {};
const { createIcon } = window.xhhaocomDataStatisticsV2Constants || {};

function initRealtimeActivity(element, embedMode) {
element.className = 'xhhaocom-dataStatistics-v2-activity';
if (showLoading) showLoading(element);

const realtimeUrl = '/apis/api.data.statistics.xhhao.com/v1alpha1/umami/realtime';

const updateActivity = () => {
if (!safeFetch) return;
safeFetch(realtimeUrl)
.then(data => {
if (!data?.events || !Array.isArray(data.events) || data.events.length === 0) {
element.innerHTML = '<div class="xhhaocom-dataStatistics-v2-activity-empty">暂无活动</div>';
return;
}

element.innerHTML = '';

const section = document.createElement('div');
section.className = 'xhhaocom-dataStatistics-v2-activity-section';

const header = document.createElement('div');
header.className = 'xhhaocom-dataStatistics-v2-activity-header';
header.innerHTML = `
<div class="xhhaocom-dataStatistics-v2-activity-title-box">
<span class="xhhaocom-dataStatistics-v2-activity-title">近30分钟网站活动</span>
<span class="xhhaocom-dataStatistics-v2-activity-badge-wrapper">
<span class="xhhaocom-dataStatistics-v2-activity-badge"></span>
<span class="xhhaocom-dataStatistics-v2-activity-badge-text">实时数据</span>
</span>
</div>
<span class="xhhaocom-dataStatistics-v2-activity-subtitle">
捕捉最新访客动态与来源
</span>
`;
section.appendChild(header);

const totals = data.totals || {};
const listContainer = document.createElement('div');
listContainer.className = 'xhhaocom-dataStatistics-v2-activity-body';

const metricsBar = document.createElement('div');
metricsBar.className = 'xhhaocom-dataStatistics-v2-activity-metrics';
const uniqueVisitors = parseInt(totals.visitors) || 0;
const totalViews = parseInt(totals.views) || 0;
const activePages = new Set();
data.events.forEach(event => {
if (event.urlPath) {
activePages.add(event.urlPath);
}
});

if (createActivityMetric) {
metricsBar.appendChild(createActivityMetric('fire', totalViews, '实时浏览量'));
metricsBar.appendChild(createActivityMetric('account', uniqueVisitors, '实时访客'));
metricsBar.appendChild(createActivityMetric('eye', activePages.size, '活跃页面数'));
}
listContainer.appendChild(metricsBar);

const maxEvents = MAX_ACTIVITY_EVENTS || 30;
const events = data.events.slice(0, maxEvents);
const list = document.createElement('div');
list.className = 'xhhaocom-dataStatistics-v2-activity-list';

events.forEach(event => {
const item = document.createElement('div');
item.className = 'xhhaocom-dataStatistics-v2-activity-item';
const time = new Date(event.createdAt);
const timeStr = formatTimeChinese ? formatTimeChinese(time) : time.toLocaleString();
const urlPath = event.urlPath || '/';

item.innerHTML = `
<div class="xhhaocom-dataStatistics-v2-activity-content">
<div class="xhhaocom-dataStatistics-v2-activity-time-line">
<span class="xhhaocom-dataStatistics-v2-activity-time">${timeStr}</span>
<span class="xhhaocom-dataStatistics-v2-activity-separator">
${createIcon ? createIcon('eye', 14) : ''}
<span>${urlPath}</span>
</span>
</div>
<div class="xhhaocom-dataStatistics-v2-activity-detail">
<span class="xhhaocom-dataStatistics-v2-activity-person">
${createIcon ? createIcon('account', 14) : ''}
</span>
<span class="xhhaocom-dataStatistics-v2-activity-text">${formatDeviceInfo ? formatDeviceInfo(event) : ''}</span>
</div>
</div>
`;

list.appendChild(item);
});
listContainer.appendChild(list);
section.appendChild(listContainer);

element.appendChild(section);
})
.catch(err => {
console.error('[Activity]', err);
element.innerHTML = '<div class="xhhaocom-dataStatistics-v2-activity-error">加载失败</div>';
});
};

updateActivity();
const interval = setInterval(updateActivity, 30000);
element.setAttribute('data-cleanup', interval);
}

window.xhhaocomDataStatisticsV2Activity = {
initRealtimeActivity
};
})();
34 changes: 34 additions & 0 deletions src/main/resources/static/js/modules/dataStatistics/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
(function() {
'use strict';

const icons = {
'chart-line': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline><polyline points="17 6 23 6 23 12"></polyline></svg>',
'account-group': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>',
'account': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>',
'fire': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path></svg>',
'lightning-bolt': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>',
'eye': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>'
};

const MAX_ACTIVITY_EVENTS = 30;
const TRAFFIC_TYPE_LABELS = {
daily: '今日概览',
weekly: '近7天趋势',
monthly: '近30天趋势',
quarterly: '近90天趋势',
yearly: '近一年趋势'
};

function createIcon(iconName, size = 24) {
const svg = icons[iconName];
if (!svg) return '';
return svg.replace('viewBox="0 0 24 24"', `viewBox="0 0 24 24" width="${size}" height="${size}"`);
}

window.xhhaocomDataStatisticsV2Constants = {
icons,
MAX_ACTIVITY_EVENTS,
TRAFFIC_TYPE_LABELS,
createIcon
};
})();
70 changes: 70 additions & 0 deletions src/main/resources/static/js/modules/dataStatistics/dom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
(function() {
'use strict';

const { createIcon } = window.xhhaocomDataStatisticsV2Constants || {};
const { formatNumber } = window.xhhaocomDataStatisticsV2Formatters || {};

function createStatCard(iconName, value, label, isRealtime = false) {
const card = document.createElement('div');
card.className = 'xhhaocom-dataStatistics-v2-traffic-card';
card.setAttribute('data-variant', isRealtime ? 'realtime' : 'history');

const iconEl = document.createElement('span');
iconEl.className = 'xhhaocom-dataStatistics-v2-traffic-icon';
iconEl.innerHTML = createIcon ? createIcon(iconName, 24) : '';

const valueEl = document.createElement('div');
valueEl.className = 'xhhaocom-dataStatistics-v2-traffic-value';
valueEl.textContent = formatNumber ? formatNumber(value) : value;

const labelEl = document.createElement('div');
labelEl.className = 'xhhaocom-dataStatistics-v2-traffic-label';
labelEl.textContent = label;

card.appendChild(iconEl);
card.appendChild(valueEl);
card.appendChild(labelEl);

if (isRealtime) {
const realtimeEl = document.createElement('div');
realtimeEl.className = 'xhhaocom-dataStatistics-v2-traffic-realtime';
realtimeEl.dataset.tooltip = '实时数据';
card.appendChild(realtimeEl);
}

return card;
}

function createActivityMetric(iconName, value, label) {
const metric = document.createElement('div');
metric.className = 'xhhaocom-dataStatistics-v2-activity-metric';

const iconEl = document.createElement('span');
iconEl.className = 'xhhaocom-dataStatistics-v2-activity-metric-icon';
iconEl.innerHTML = createIcon ? createIcon(iconName, 18) : '';

const contentEl = document.createElement('div');
contentEl.className = 'xhhaocom-dataStatistics-v2-activity-metric-content';

const valueEl = document.createElement('div');
valueEl.className = 'xhhaocom-dataStatistics-v2-activity-metric-value';
valueEl.textContent = formatNumber ? formatNumber(value) : value;

const labelEl = document.createElement('div');
labelEl.className = 'xhhaocom-dataStatistics-v2-activity-metric-label';
labelEl.textContent = label;

contentEl.appendChild(valueEl);
contentEl.appendChild(labelEl);

metric.appendChild(iconEl);
metric.appendChild(contentEl);

return metric;
}

window.xhhaocomDataStatisticsV2DOM = {
createStatCard,
createActivityMetric
};
})();
70 changes: 70 additions & 0 deletions src/main/resources/static/js/modules/dataStatistics/formatters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
(function() {
'use strict';

const { getCountryName } = window.xhhaocomDataStatisticsV2I18n || {};

function formatNumber(num) {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
}

function formatTimeChinese(date) {
const hours = date.getHours();
const minutes = date.getMinutes();
const seconds = date.getSeconds();
const period = hours >= 12 ? '下午' : '上午';
const displayHours = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours);
return `${period} ${String(displayHours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}

function formatDeviceInfo(event) {
const osMap = {
'Mac OS': 'macOS',
'Windows': 'Windows',
'Android': 'Android',
'iOS': 'iOS',
'Linux': 'Linux'
};
const deviceMap = {
'desktop': '桌面电脑',
'mobile': '手机',
'tablet': '平板电脑',
'laptop': '笔记本'
};

let browser = event.browser || '';
if (browser && browser.toLowerCase().includes('webview')) {
if (!browser.includes('(') || !browser.includes(')')) {
browser = browser.replace(/\s*webview\s*/gi, ' (webview)');
}
}

const country = getCountryName ? getCountryName(event.country) : '';
const os = event.os ? (osMap[event.os] || event.os) : '';
const device = event.device ? (deviceMap[event.device] || event.device) : '';

let description = country ? `来自 ${country} 的访客` : '一位访客';

if (os && device) {
description += `在搭载 ${os} 的 ${device} 上`;
} else if (os) {
description += `在搭载 ${os} 的设备上`;
} else if (device) {
description += `在 ${device} 上`;
}

description += browser ? `使用 ${browser} 浏览器进行访问。` : '进行访问。';

return description;
}

window.xhhaocomDataStatisticsV2Formatters = {
formatNumber,
formatTimeChinese,
formatDeviceInfo
};
})();
Loading