二次开发萌化VisitorLoggerPro插件
其实之前我一直在用VisitorLoggerPro这个插件,但是由于之前的版本不够完善,也没想着进一步提升改造。前几天偶然翻了翻这个项目的GitHub仓库,发现基本上更新完善了,上一次更改已经是三个月之前,遂下了最新版回来尝试一下,发现功能非常满意。
但是,美中不足的是,插件从jsdelivr.net下载相关的静态文件容易卡半天加载不出来。虽然插件写有智能回退使用网站的服务器进行加载,但是我发现它缺失了文件且返回路径不对,导致如果jsdelivr.net无法访问,页面会加载异常。
于是,就萌生了修复这个插件的问题并且改造进行个性化的想法。
打开了后台页面观摩了一下,并用浏览器的开发者工具随手调了一下CSS样式,最终明确了个性化的路径:
- 设置一个全局背景图
- 看板卡片进行半透明化
于是说干就干,我先Fork了原项目,想了想要不就取名VisitorLoggerPro-Enhanced吧!然后开始了我的折腾和修改之旅。
Github项目地址:VisitorLoggerPro-Enhanced
可以去直接下载使用。
修复静态文件回退到本地失败
这肯定是最先需要解决的问题。如果不解决这个问题,该插件在我这里就处于不可用状态。
补回缺失的本地文件
这一步对于有网站搭建经验的人很简单,F12打开浏览器开发者工具,然后刷新页面观察网页都发送了那些外链请求,一下子就抓出来了:
- 本地缺失的js:
https://cdn.jsdelivr.net/npm/flatpickr - 本地缺失的css:
https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css
其中,flatpickr抓取回来是没有后缀的,得加上个.js后缀变成flatpickr.js。
然后对号入座,flatpickr.js放到js/目录,新建一个css/目录放置flatpickr.min.css。
补回文件后的插件目录文件树应该是这样的:
tree
.
├── action
│ └── visitor-stats.php
├── Action.php
├── adapter.php
├── css
│ └── flatpickr.min.css
├── getTrendData.php
├── getVisitStatistic.php
├── ip2region
│ └── src
│ ├── ip2region.xdb
│ └── XdbSearcher.php
├── ipdata
│ └── src
│ ├── ipdbv6.func.php
│ ├── IpLocation.php
│ ├── ipv6wry-country.db
│ ├── ipv6wrys.db
│ ├── qqwry.dat
│ ├── qqwrye.dat
│ └── zxipv6wry.db
├── js
│ ├── echarts.min.js
│ ├── flatpickr.js
│ └── README.md
├── panel.php
├── Plugin.php
├── README.md
├── trend.php
└── visitor-stats.php
8 directories, 23 files修复完善静态文件获取逻辑
原本插件本地静态文件的相对链接写法是:
localScript.src = './js/echarts.min.js';这会导致获取文件时发生404错误,因为在插件页面上下文中,该相对路径会被解析为https://www.nekopara.uk/admin/js/echarts.min.js,那包404的。
正确的URL连接位置应该是https://www.nekopara.uk/usr/plugins/VisitorLoggerPro/js/echarts.min.js,所以,这个相对链接应该改成../usr/plugins/VisitorLoggerPro/js/echarts.min.js。
而且,原插件仅实现了echarts.min.js的本地回退逻辑,未处理另外两个静态文件(flatpickr.js和flatpickr.min.css),因此即使修复了路径,还是会卡加载。
还有一个致命的缺陷是,原本的回退本地源的逻辑是等待失败,但是一等就是大几十秒,非常影响体验。于是我就设置了超时时间,一旦第三方的静态文件加载时间超过2秒,立即回退到本地,避免长时间等待。
以下是完善后的逻辑代码:
对于panel.php内嵌js部分的修改,在原文件64行的地方开始修改:
<script>
// 为每个资源添加超时加载机制
function loadScriptWithTimeout(src, timeout = 2000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('Timeout'));
}, timeout);
const script = document.createElement('script');
script.src = src;
script.onload = () => {
clearTimeout(timer);
resolve();
};
script.onerror = () => {
clearTimeout(timer);
reject(new Error('Failed to load'));
};
document.head.appendChild(script);
});
}
// 加载ECharts的智能回退机制
function loadECharts() {
return new Promise((resolve, reject) => {
// 首先尝试CDN,2秒超时
loadScriptWithTimeout('https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js', 2000)
.then(() => {
console.log('✅ ECharts CDN加载成功');
resolve('cdn');
})
.catch(() => {
console.warn('⚠️ ECharts CDN加载失败,尝试本地文件');
// CDN失败,立即尝试本地文件
const localScript = document.createElement('script');
localScript.src = '../usr/plugins/VisitorLoggerPro/js/echarts.min.js';
localScript.onload = () => {
console.log('✅ ECharts 本地文件加载成功');
resolve('local');
};
localScript.onerror = () => {
console.error('❌ ECharts 本地文件也加载失败');
reject('both_failed');
};
document.head.appendChild(localScript);
});
});
}
// 加载Flatpickr的智能回退机制
function loadFlatpickr() {
return new Promise((resolve, reject) => {
// 首先尝试CDN,2秒超时
loadScriptWithTimeout('https://cdn.jsdelivr.net/npm/flatpickr', 2000)
.then(() => {
console.log('✅ Flatpickr CDN加载成功');
// 加载CDN的CSS
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css';
document.head.appendChild(link);
resolve('cdn');
})
.catch(() => {
console.warn('⚠️ Flatpickr CDN加载失败,尝试本地文件');
// CDN失败,立即尝试本地文件
const localScript = document.createElement('script');
localScript.src = '../usr/plugins/VisitorLoggerPro/js/flatpickr.js';
localScript.onload = () => {
console.log('✅ Flatpickr 本地文件加载成功');
// 加载本地CSS
const cssLink = document.createElement('link');
cssLink.rel = 'stylesheet';
cssLink.href = '../usr/plugins/VisitorLoggerPro/css/flatpickr.min.css';
document.head.appendChild(cssLink);
resolve('local');
};
localScript.onerror = () => {
console.error('❌ Flatpickr 本地文件也加载失败');
reject('both_failed');
};
document.head.appendChild(localScript);
});
});
}
// 并行加载所有资源
Promise.allSettled([loadECharts(), loadFlatpickr()]).then(results => {
console.log('📊 资源加载结果:', results);
// 确保DOM加载完成
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeApp);
} else {
initializeApp();
}
});
function initializeApp() {
// 确保在资源加载完成后执行初始化
if (typeof window.startChartInitialization === 'function') {
window.startChartInitialization();
}
}
</script>对于visitor-stats.php内嵌js部分的修改,在原文件90行的地方开始修改:
<!-- 智能加载ECharts:优先CDN,失败时自动回退到本地 -->
<script>
// 添加超时机制的脚本加载函数
function loadScriptWithTimeout(src, timeout = 2000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('Timeout'));
}, timeout);
const script = document.createElement('script');
script.src = src;
script.onload = () => {
clearTimeout(timer);
resolve();
};
script.onerror = () => {
clearTimeout(timer);
reject(new Error('Failed to load'));
};
document.head.appendChild(script);
});
}
// 优化后的ECharts加载函数
function loadEChartsWithFallback() {
return new Promise((resolve, reject) => {
// 优先尝试CDN,2秒超时
loadScriptWithTimeout('https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js', 2000)
.then(() => {
console.log('✅ ECharts CDN加载成功');
resolve('cdn');
})
.catch(() => {
console.warn('⚠️ ECharts CDN加载超时或失败,立即尝试本地文件');
// CDN失败,立即加载本地文件
const localScript = document.createElement('script');
localScript.src = '../usr/plugins/VisitorLoggerPro/js/echarts.min.js';
localScript.onload = () => {
console.log('✅ ECharts 本地文件加载成功');
resolve('local');
};
localScript.onerror = () => {
console.error('❌ ECharts 本地文件加载失败');
reject('both_failed');
};
document.head.appendChild(localScript);
});
});
}
// 立即开始加载
loadEChartsWithFallback().then(result => {
console.log('📊 ECharts加载完成:', result);
// 设置全局标记,表示ECharts已准备就绪
window.echartsReady = true;
// 触发应用初始化(如果需要)
if (typeof window.startTrendInitialization === 'function') {
window.startTrendInitialization();
}
}).catch(error => {
console.error('❌ ECharts加载完全失败:', error);
window.echartsReady = false;
// 可选:显示错误提示
const errorDiv = document.createElement('div');
errorDiv.innerHTML = '<div style="color: red; padding: 10px; background: #ffebee; border: 1px solid #ff8a8a;">ECharts加载失败,请检查网络或联系管理员</div>';
document.body.appendChild(errorDiv);
});
</script>对于trend.php内嵌js部分的修改,在原文件12行的地方开始修改:
<!-- 智能加载ECharts:优先CDN,失败时自动回退到本地 -->
<script>
// 为每个资源添加超时加载机制
function loadScriptWithTimeout(src, timeout = 2000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('Timeout'));
}, timeout);
const script = document.createElement('script');
script.src = src;
script.onload = () => {
clearTimeout(timer);
resolve();
};
script.onerror = () => {
clearTimeout(timer);
reject(new Error('Failed to load'));
};
document.head.appendChild(script);
});
}
// 加载ECharts的智能回退机制
function loadECharts() {
return new Promise((resolve, reject) => {
// 首先尝试CDN,2秒超时
loadScriptWithTimeout('https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js', 2000)
.then(() => {
console.log('✅ ECharts CDN加载成功');
resolve('cdn');
})
.catch(() => {
console.warn('⚠️ ECharts CDN加载失败,尝试本地文件');
// CDN失败,立即尝试本地文件
const localScript = document.createElement('script');
localScript.src = '../usr/plugins/VisitorLoggerPro/js/echarts.min.js';
localScript.onload = () => {
console.log('✅ ECharts 本地文件加载成功');
resolve('local');
};
localScript.onerror = () => {
console.error('❌ ECharts 本地文件也加载失败');
reject('both_failed');
};
document.head.appendChild(localScript);
});
});
}
// 加载Flatpickr的智能回退机制
function loadFlatpickr() {
return new Promise((resolve, reject) => {
// 首先尝试CDN,2秒超时
loadScriptWithTimeout('https://cdn.jsdelivr.net/npm/flatpickr', 2000)
.then(() => {
console.log('✅ Flatpickr CDN加载成功');
// 加载CDN的CSS
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css';
document.head.appendChild(link);
resolve('cdn');
})
.catch(() => {
console.warn('⚠️ Flatpickr CDN加载失败,尝试本地文件');
// CDN失败,立即尝试本地文件
const localScript = document.createElement('script');
localScript.src = '../usr/plugins/VisitorLoggerPro/js/flatpickr.js';
localScript.onload = () => {
console.log('✅ Flatpickr 本地文件加载成功');
// 加载本地CSS
const cssLink = document.createElement('link');
cssLink.rel = 'stylesheet';
cssLink.href = '../usr/plugins/VisitorLoggerPro/css/flatpickr.min.css';
document.head.appendChild(cssLink);
resolve('local');
};
localScript.onerror = () => {
console.error('❌ Flatpickr 本地文件也加载失败');
reject('both_failed');
};
document.head.appendChild(localScript);
});
});
}
// 并行加载所有资源
Promise.allSettled([loadECharts(), loadFlatpickr()]).then(results => {
console.log('📊 资源加载结果:', results);
// 确保DOM加载完成
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeApp);
} else {
initializeApp();
}
});
function initializeApp() {
if (typeof window.startTrendInitialization === 'function') {
window.startTrendInitialization();
}
}
</script>对实现显示图表的trend.php,panel.php和visitor-stats.php的修复主要解决以下核心问题:
- 加载超时问题:原插件等待第三方CDN加载超时(大几十秒),现添加2秒超时机制,一旦CDN加载超时立即回退到本地,极大提升加载体验。
- 加载逻辑完善:原逻辑仅处理
echarts.min.js,现完整支持echarts.min.js、flatpickr.js和flatpickr.min.css的智能加载与回源。 - 错误处理增强:添加加载成功/失败日志,以及加载失败时的用户提示,提升用户体验与问题排查效率。
以上修复确保在jsdelivr.net不可用时,插件仍能快速加载并正常运行,同时保持原有功能完整性。修复我已经为GitHub上的原项目提交了的Pull Requests,能不能被合并就不知道了(
接下来开始搞我的个性化了)
萌化VisitorLoggerPro
首先,最简单粗暴的萌化方式就是加上二次元背景图,这是最直接,最有效果的方式。
我的修改思路是,先在浏览器开发者工具里面直接修改网页代码查看效果,然后再针对性去修改插件里面对应的代码。
为插件添加背景图
在插件界面,右键点击页面边缘的空白处,选择检查。然后在HTML代码区一路上划找到页面最底层的<div>盒子,发现是<div class="main">,然后针对性去找CSS样式表,真给我找到了:
.main {
padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
}这时可以先在浏览器里面修改CSS样式表看看效果:
.main {
padding: 20px;
min-height: 100vh;
background-image: url('https://teachermate.oss-cn-qingdao.aliyuncs.com/irGnf-1725727799259-neko4_n33b.png');
background-repeat: no-repeat;
background-position: center center;
background-attachment: fixed;
background-size: cover;
}瞬间就对味了一大截:
然后就需要修改规则了,在全部文件中检索.main这个CSS类选择器,然后进行代码修改。
好了,那么现在问题来了,我虽然是可以直接找到对应的代码写死URL在上面,但是如果别人想换个图片,就没这么方便了。于是我就在思考,能不能把这个参数作为应该可以给用户设置的插件参数?
得益于Typecho插件开发的便利性,答案是肯定的,只需要在插件设置界面给出设置插件的参数的地方,在后面通过Helper::options()获取这个参数即可。
找Plugin.php添加多一个设置选项:插件背景设置
/* 插件背景设置 */
$backgroundUrl = new Typecho_Widget_Helper_Form_Element_Text(
'backgroundUrl',
null,
'https://pic.nekopara.uk/?format=webp', // 默认值
_t('插件背景设置'),
_t('可填写图片API的URL(如随机图片API)或具体图片的URL直链。默认使用猫娘乐园图片API')
);
$form->addInput($backgroundUrl);我这里定义参数名称为backgroundUrl,然后在插件代码里面进行获取的方式就是:
$backgroundUrl = Helper::options()->plugin('VisitorLoggerPro')->backgroundUrl ?: 'https://pic.nekopara.uk/?format=webp';在嵌入到PHP的CSS样式表部分,我只需要这样子修改:
.main {
padding: 20px;
min-height: 100vh;
background-image: url('<?php echo $backgroundUrl; ?>');
background-repeat: no-repeat;
background-attachment: fixed;
background-position: center center;
background-size: cover;
}就可以根据设置的URL返回特定的样式表了,让用户可以自定义背景图的问题也就迎刃而解了。
插件默认的设置图片来源是我的猫娘乐园CG图API,可以自行更换为其他图片直链或者图片API。
为卡片设置半透明颜色
但是,只是修改一个背景图,卡片是不透明的把好看的图片都基本上档完了,多没意思?于是,我就想在保证可读性的情况下,让卡片半透明化,让背景图能被看到。
继续F12大法,找到每个卡片对应的CSS规则:
访客日志页面部分,对应panel.php文件:
.page-header {
background: #fff;
border-radius: 12px;
padding: 20px;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
border: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.action-form {
background: #fff;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
border: 1px solid #e2e8f0;
display: flex;
flex-direction: column;
}
.logs-section {
background: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
/* height: calc(100vh - 200px); */
overflow: auto;
min-width: 0;
border: 1px solid #e2e8f0;
}
.stats-section {
background: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
/* height: calc(100vh - 200px); */
display: flex;
flex-direction: column;
gap: 24px;
border: 1px solid #e2e8f0;
}
.chart-container {
flex: 1;
min-height: 260px;
background: linear-gradient(135deg, #fff 0%, #f8fafc 100%);
border-radius: 12px;
padding: 8px;
border: 1px solid #e2e8f0;
position: relative;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
overflow: hidden;
}访客日志趋势分析部分,对应trend.php文件:
.page-header {
background: #fff;
border-radius: 12px;
padding: 20px;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
border: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.controls-section {
background: #fff;
border-radius: 12px;
padding: 20px;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
border: 1px solid #e2e8f0;
}
.trend-section {
background: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
border: 1px solid #e2e8f0;
}
.stats-summary {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 20px;
padding: 16px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
flex-wrap: wrap;
}
.metrics-explanation {
background: #fff;
border-radius: 12px;
padding: 24px;
margin-top: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
border: 1px solid #e2e8f0;
}
.metric-card {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 20px;
transition: all 0.3s ease;
}
.technical-notes {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 20px;
border-left: 4px solid #3498db;
}只需要将每个卡片的background: #fff;定义颜色部分设置成半透明即可。
虽然最简单的方法就是保持原来的颜色,增加透明度,但是我考虑到这一片卡片后面如果是靠修改代码调整,调整起来也很花时间,不如像图片背景那样给用户输入一个颜色自己配。
同样的方法,在Plugin.php中增加配置选项:
/* 插件卡片颜色设置 */
$backgroundColour = new Typecho_Widget_Helper_Form_Element_Text(
'backgroundColour',
null,
'#ffffffc4', // 默认值
_t('插件展示内容卡片颜色设置,采用HTML颜色代码')
);
$form->addInput($backgroundColour);然后在panel.php和trend.php加上:
// 获取配置的卡片背景色(带默认值)
$backgroundColour = Helper::options()->plugin('VisitorLoggerPro')->backgroundColour ?: '#ffffffc4';并针对性的设置需要设置的颜色CSS代码位置,例如:
.page-header {
background: <?php echo $backgroundColour; ?>;
border-radius: 12px;
padding: 20px;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
border: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
}找到上面说的CSS代码,将背景颜色代码部分替换为<?php echo $backgroundColour; ?>就完事了。
这次的自定义修改,让我对Typecho的插件和PHP代码逻辑有了更进一步的认识,虽然仍是面向LLM编程,但是至少有实现是思路和方法,不会具体的语法和代码,倒是可以让LLM来写。
效果展示
最后展示一下萌化后的效果:

这下暴露出小破站访问量了,果然是没什么人看的,被打的次数倒是不少(
番外:使用方法
进入你的Typecho部署目录,然后进入插件文件夹:
cd /data/typecho/
cd usr/plugins/随后,克隆项目并重命名文件夹:
git clone https://github.com/Chocola-X/VisitorLoggerPro-Enhanced
mv VisitorLoggerPro-Enhanced/ VisitorLoggerPro/删除无用文件(可选):
cd VisitorLoggerPro/
rm -rf .git/
rm -rf .gitignore
rm README.md 然后进入Typecho后台启用插件即可。









































































































































































































