二次开发萌化VisitorLoggerPro插件

其实之前我一直在用VisitorLoggerPro这个插件,但是由于之前的版本不够完善,也没想着进一步提升改造。前几天偶然翻了翻这个项目的GitHub仓库,发现基本上更新完善了,上一次更改已经是三个月之前,遂下了最新版回来尝试一下,发现功能非常满意。
但是,美中不足的是,插件从jsdelivr.net下载相关的静态文件容易卡半天加载不出来。虽然插件写有智能回退使用网站的服务器进行加载,但是我发现它缺失了文件且返回路径不对,导致如果jsdelivr.net无法访问,页面会加载异常。
于是,就萌生了修复这个插件的问题并且改造进行个性化的想法。
打开了后台页面观摩了一下,并用浏览器的开发者工具随手调了一下CSS样式,最终明确了个性化的路径:

  1. 设置一个全局背景图
  2. 看板卡片进行半透明化

于是说干就干,我先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.jsflatpickr.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.phppanel.phpvisitor-stats.php的修复主要解决以下核心问题:

  1. 加载超时问题:原插件等待第三方CDN加载超时(大几十秒),现添加2秒超时机制,一旦CDN加载超时立即回退到本地,极大提升加载体验。
  2. 加载逻辑完善:原逻辑仅处理echarts.min.js,现完整支持echarts.min.jsflatpickr.jsflatpickr.min.css的智能加载与回源。
  3. 错误处理增强:添加加载成功/失败日志,以及加载失败时的用户提示,提升用户体验与问题排查效率。

以上修复确保在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;
}

瞬间就对味了一大截:
x_20251104_103817.png
然后就需要修改规则了,在全部文件中检索.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.phptrend.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来写。

效果展示

最后展示一下萌化后的效果:
x_20251104_154439.png
x_20251104_154542.png
这下暴露出小破站访问量了,果然是没什么人看的,被打的次数倒是不少(

番外:使用方法

进入你的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后台启用插件即可。