前言
之前访问张洪HEO博客网站的时候发现底部有个服务状态显示,于是就想自己搞一个。
教程
本教程基于solitude主题,如果使用其他主题,请自行修改。
1.将如下代码替换themes\solitude\layout\includes\footer.pug文件中的代码:
- var { information , group } = theme.footer
if theme.post.footer.enable && is_post()
div#st-footer-bar
div.footer-logo
if theme.site.name.class === 'i_class'
i.solitude(class=theme.site.name.custom)
else if theme.site.name.class === 'img'
img.solitude.nolazyload(src=url_for(theme.site.name.custom))
else if theme.site.name.class === 'text'
span= theme.site.name.custom
div.footer-bar-description=theme.post.footer.desc
if theme.post.footer.button.enable
a.footer-bar-link(href=url_for(theme.post.footer.button.url))= theme.post.footer.button.name
if information.left || information.right || information.author
div#footer_deal
- var leftInfo = information.left || []
- var rightInfo = information.right || []
each value, label in leftInfo
- var array = value.split('||')
a.deal_link(href=url_for(trim(array[0])), title=label)
i.solitude(class=array[1])
if information.author
div#footer_mini_logo.nolazyload.footer_mini_logo(title=_p('nav.backtop'), onclick="sco.toTop()")
img(src=information.author, alt=_p('nav.backtop'))
each value, label in rightInfo
- var array = value.split('||')
a.deal_link(href=url_for(trim(array[0])), title=label)
i.solitude(class=array[1])
if group
div#st-footer
each value, x in group || []
div.footer-group
h3.footer-title= x
div.footer-links
each url, y in value
a.footer-item(href=url_for(url), title=y)= y
if theme.footer && theme.footer.randomlink
div.footer-group
div.footer-title-group
h3.footer-title= _p('footer.randomlink')
button.random-friends-btn(onclick='randomLinksList()', title=_p('footer.randomlink'))
i.solitude.fas.fa-arrows-rotate
div.footer-links#friend-links-in-footer
div#footer-bar
div.footer-bar-links
div.footer-bar-left
if moment(theme.aside.siteinfo.runtime).year() === new Date().getFullYear()
div.copyright © #{moment(theme.aside.siteinfo.runtime).year()} By
a.footer-bar-link(href=url_for("/"))
img.author-avatar(src=url_for(theme.site.icon))
= config.title
// 服务状态显示
a.footer-service-status(href="Uptime Kuma 状态页面URL" target="_blank" title="查看项目状态")
span.service-status-indicator#serviceStatusIndicator
span.service-status-text#serviceStatusText 加载中...
.beian-group
if theme.footer.beian
- var beian = theme.footer.beian || []
each item in beian
a.footer-bar-link(href=url_for(item.url), title=item.name)
if item.icon
img.beian-icon(src=url_for(item.icon), alt=item.name)
span.beian-name= item.name
else
div.copyright © #{moment(theme.aside.siteinfo.runtime).year()} - #{new Date().getFullYear()} By
a.footer-bar-link(href=url_for("/"))
img.author-avatar(src=url_for(theme.site.icon))
= config.title
// 服务状态显示
a.footer-service-status(href="Uptime Kuma 状态页面URL" target="_blank" title="查看项目状态")
span.service-status-indicator#serviceStatusIndicator
span.service-status-text#serviceStatusText 加载中...
.beian-group
if theme.footer.beian
- var beian = theme.footer.beian || []
each item in beian
a.footer-bar-link(href=url_for(item.url), title=item.name)
if item.icon
img.beian-icon(src=url_for(item.icon), alt=item.name)
span.beian-name= item.name
if theme.footer.links
div.footer-bar-right
each item in theme.footer.links
if item.icon
a.footer-bar-link(href=url_for(item.url), alt=item.name)
each icon in item.icon
i.solitude(class=icon)
else if item.img
a.footer-bar-link(href=url_for(item.url), alt=item.name)
img(src=url_for(item.img), alt=item.name)
else
a.footer-bar-link(href=url_for(item.url), alt=item.name)!= item.name
if theme.comment.use && theme.comment.commentBarrage
div.comment-barrage.needEndHide
2.在source\js\custom.js文件中添加如下代码:
//uptime检测
class ServiceStatus {
constructor() {
this.isInitialized = false;
this.apiUrl = 'Uptime Kuma 状态页面URL api';
// 在这里直接配置你的 Uptime Kuma 状态页面URL
this.statusPageUrl = 'Uptime Kuma 状态页面URL api';
this.config = {
statusText: {
normal: '服务正常',
warning: '部分异常',
error: '服务异常',
loading: '加载中...',
unknown: '未知状态',
noApi: '未配置API',
apiError: 'API错误',
networkError: '网络错误',
noService: '无监控服务'
}
};
// 直接初始化
this.init();
this.injectStyles();
}
injectStyles() {
// 注入CSS样式
const style = document.createElement('style');
style.textContent = `
.footer-service-status {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: 8px;
color: var(--efu-fontcolor);
font-size: 0.75rem;
font-weight: 400;
white-space: nowrap;
text-decoration: none;
transition: color 0.3s ease;
vertical-align: middle;
}
.footer-service-status:hover {
color: var(--efu-theme);
}
.service-status-indicator {
width: 5px;
height: 5px;
border-radius: 50%;
display: inline-block;
transition: all 0.3s ease;
flex-shrink: 0;
}
.service-status-indicator.status-normal {
background: #52c41a;
box-shadow: 0 0 4px rgba(82, 196, 26, 0.4);
}
.service-status-indicator.status-warning {
background: #faad14;
box-shadow: 0 0 4px rgba(250, 173, 20, 0.4);
}
.service-status-indicator.status-error {
background: #ff4d4f;
box-shadow: 0 0 4px rgba(255, 77, 79, 0.4);
}
.service-status-indicator.status-unknown {
background: #d9d9d9;
box-shadow: 0 0 4px rgba(217, 217, 217, 0.4);
}
.service-status-indicator.status-loading {
background: #1890ff;
box-shadow: 0 0 4px rgba(24, 144, 255, 0.4);
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.1);
}
100% {
opacity: 1;
transform: scale(1);
}
}
.service-status-text {
font-weight: 400;
transition: color 0.3s ease;
font-size: 0.75rem;
}
/* 确保copyright内的元素正确对齐 */
.copyright {
display: flex;
align-items: center;
flex-wrap: wrap;
flex-direction: row !important;
}
/* 覆盖footer-bar-left的column布局,让copyright和服务状态在同一行 */
.footer-bar-left {
flex-direction: row !important;
align-items: center;
flex-wrap: wrap;
}
@media (max-width: 768px) {
.footer-service-status {
margin-left: 6px;
}
.service-status-indicator {
width: 4px;
height: 4px;
}
.service-status-text {
font-size: 0.7rem;
}
/* 移动端保持垂直布局 */
.footer-bar-left {
flex-direction: column !important;
align-items: center;
}
}
`;
document.head.appendChild(style);
}
init() {
// 等待DOM加载完成
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.initStatus());
} else {
this.initStatus();
}
// Pjax兼容 - 页面更新时重新获取状态
document.addEventListener('pjax:complete', () => this.initStatus());
}
initStatus() {
const statusIndicator = document.getElementById('serviceStatusIndicator');
const statusText = document.getElementById('serviceStatusText');
if (!statusIndicator || !statusText) return;
this.isInitialized = true;
this.fetchServiceStatus();
// 已移除定时刷新,以避免每60秒自动刷新
}
async fetchServiceStatus() {
try {
const servicesData = await this.getServicesData();
if (servicesData && servicesData.length > 0) {
this.updateStatus(servicesData);
} else {
this.updateStatus([]);
}
} catch (error) {
console.error('获取服务状态失败:', error);
// 错误时也显示加载中状态
this.updateStatus([]);
}
}
async getServicesData() {
try {
if (!this.statusPageUrl) {
throw new Error('Status page URL not configured');
}
const response = await fetch(this.statusPageUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// 检查heartbeatList数据结构
if (!data.heartbeatList || Object.keys(data.heartbeatList).length === 0) {
return [];
}
// 转换heartbeatList数据格式
const services = Object.keys(data.heartbeatList).map(monitorId => {
const heartbeats = data.heartbeatList[monitorId];
// 获取最新的心跳状态
const latestHeartbeat = heartbeats[heartbeats.length - 1];
const status = latestHeartbeat ? latestHeartbeat.status : 0;
const lastUpdateTime = latestHeartbeat ? latestHeartbeat.time : '';
// 计算可用性(从uptimeList获取)
const uptimeKey = `${monitorId}_24`;
const uptime = data.uptimeList && data.uptimeList[uptimeKey] ? data.uptimeList[uptimeKey] : 0;
return {
id: monitorId,
name: `服务 ${monitorId}`,
status: this.mapUptimeKumaStatus(status),
uptime: parseFloat(uptime || '0'),
url: `#${monitorId}`,
lastUpdateTime: lastUpdateTime
};
});
return services;
} catch (error) {
console.error('获取服务数据失败:', error);
throw error;
}
}
mapUptimeKumaStatus(uptimeStatus) {
// Uptime Kuma状态映射
// 0: down, 1: up, 2: maintenance, 3: pending
switch (uptimeStatus) {
case 1:
return 'normal';
case 0:
return 'error';
case 2:
return 'warning';
case 3:
return 'loading';
default:
return 'unknown';
}
}
updateStatus(services) {
const statusIndicator = document.getElementById('serviceStatusIndicator');
const statusText = document.getElementById('serviceStatusText');
const serviceStatusLink = document.querySelector('.footer-service-status');
if (!statusIndicator || !statusText) {
return;
}
if (services.length === 0) {
statusIndicator.className = 'service-status-indicator status-loading';
statusText.textContent = this.config.statusText.loading;
if (serviceStatusLink) {
serviceStatusLink.title = '查看项目状态';
}
return;
}
const totalServices = services.length;
const normalServices = services.filter(s => s.status === 'normal').length;
const warningServices = services.filter(s => s.status === 'warning').length;
const errorServices = services.filter(s => s.status === 'error').length;
let status = 'normal';
let statusTextContent = '';
if (errorServices > 0) {
status = 'error';
statusTextContent = `${errorServices}个服务异常`;
} else if (warningServices > 0) {
status = 'warning';
statusTextContent = `${warningServices}个服务警告`;
} else if (normalServices === totalServices) {
status = 'normal';
statusTextContent = '所有服务正常';
} else {
status = 'warning';
statusTextContent = `${normalServices}/${totalServices}个服务正常`;
}
// 获取最新的更新时间
const latestUpdateTime = this.getLatestUpdateTime(services);
// 更新状态指示器和文本
statusIndicator.className = `service-status-indicator status-${status}`;
statusText.textContent = statusTextContent;
// 更新title属性
if (serviceStatusLink) {
serviceStatusLink.title = `查看项目状态\n最后更新: ${latestUpdateTime}`;
}
}
getLatestUpdateTime(services) {
if (!services || services.length === 0) {
return '未知';
}
// 获取所有服务的最新更新时间
const updateTimes = services
.map(service => service.lastUpdateTime)
.filter(time => time) // 过滤掉空值
.map(time => {
// 解析时间并在API返回的基础上加8小时
const apiTime = new Date(time);
const adjustedTime = new Date(apiTime.getTime() + 8 * 60 * 60 * 1000);
return adjustedTime;
});
if (updateTimes.length === 0) {
return '未知';
}
// 找到最新的时间
const latestTime = new Date(Math.max(...updateTimes));
// 直接显示具体时间,不计算相对时间
return latestTime.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
}
// 初始化
new ServiceStatus();
3.修改其中的Uptime Kuma 状态页面URL和Uptime Kuma 状态页面URL api。
4.Uptime Kuma 状态页面URL就是你的状态页面。
5.Uptime Kuma 状态页面URL api就是访问状态页面的时候通过浏览器开发者工具中可以查看到一个有你页面名称的api,然后把这个api复制到这里进行替换即可。
6.最后一键三连就可以看到效果了。