添加来访者卡片

2048 字
10 分钟
添加来访者卡片

前言#

本文参考自青桔气球 前段时间Nice猫的api失效了,这导致我本来的来访者卡片无法使用了,于是决定改用小小api,遂有了这篇文章。

教程部分#

这次的教程使用小小api,显示来访者地址和ip

创建js文件#

[root]\hexo\source\js路径创建welcome.js,写入代码。

welcome.js
window.IP_CONFIG = {
CACHE_DURATION: 1000 * 60 * 60,
HOME_PAGE_ONLY: true,
};
const insertAnnouncementComponent = () => {
const announcementCards = document.querySelectorAll('.card-widget.card-announcement');
if (!announcementCards.length) return;
if (IP_CONFIG.HOME_PAGE_ONLY && !isHomePage()) {
announcementCards.forEach(card => card.remove());
return;
}
if (!document.querySelector('#welcome-info')) return;
fetchIpInfo();
};
const getWelcomeInfoElement = () => document.querySelector('#welcome-info');
// 获取 IP 数据
const fetchIpData = async () => {
const response = await fetch('https://v2.xxapi.cn/api/ua');
if (!response.ok) throw new Error('网络响应不正常');
const result = await response.json();
if (result?.code !== 200 || !result?.data?.address) {
throw new Error('API 返回数据异常');
}
return result;
};
// 地址字符串解析
const parseAddress = (address) => {
if (!address) return { country: '其他', province: '', city: '' };
let country = '其他';
let addr = address.trim();
if (addr.startsWith('中国')) {
country = '中国';
addr = addr.substring(2);
} else if (addr.includes('台湾')) {
country = '台湾';
} else if (addr.includes('香港')) {
country = '香港特别行政区';
} else if (addr.includes('澳门')) {
country = '澳门特别行政区';
} else if (/[A-Za-z]/.test(addr)) {
country = addr.split(',')[0]?.trim() || '其他';
return { country, province: '', city: '' };
}
if (country !== '中国') {
return { country, province: '', city: '' };
}
const provinces = [
'内蒙古自治区', '广西壮族自治区', '西藏自治区', '宁夏回族自治区', '新疆维吾尔自治区',
'香港特别行政区', '澳门特别行政区',
'北京市', '天津市', '上海市', '重庆市',
'河北省', '山西省', '辽宁省', '吉林省', '黑龙江省',
'江苏省', '浙江省', '安徽省', '福建省', '江西省', '山东省',
'河南省', '湖北省', '湖南省', '广东省', '海南省',
'四川省', '贵州省', '云南省', '陕西省', '甘肃省', '青海省',
'台湾省'
].sort((a, b) => b.length - a.length);
let province = '';
let remaining = addr;
for (const prov of provinces) {
if (addr.startsWith(prov)) {
province = prov;
remaining = addr.substring(prov.length);
break;
}
}
let city = '未知';
if (remaining) {
remaining = remaining.replace(/^[省市自治区]/, '');
const cityMatch = remaining.match(/^([\u4e00-\u9fa5]+?(?:市 | 地区 | 自治州 ||||| 旗)?)$/);
if (cityMatch && cityMatch[1]) {
city = cityMatch[1];
}
if (['北京市', '天津市', '上海市', '重庆市'].includes(province)) {
city = province.replace('市', '') + (remaining ? ' ' + remaining : '');
}
}
const provinceKey = province
.replace('省', '')
.replace('自治区', '')
.replace('特别行政区', '')
.replace('市', '');
return { country, province, provinceKey, city };
};
const formatIpDisplay = (ip) => {
if (!ip) return '未知';
return ip.includes(":") ? "<br>好复杂,咱看不懂~(ipv6)" : ip;
};
const formatLocation = (country, province, city) => {
if (!country) return '神秘地区';
if (country === '中国') {
if (['北京市', '天津市', '上海市', '重庆市'].includes(province)) {
return province;
}
if (city && city !== '未知') {
return `${province} ${city}`;
}
return province;
}
return country;
};
const generateWelcomeMessage = (pos, ipDisplay, greeting) => `
<div class="welcome-content">
<div class="welcome-line">欢迎来自 <b>${pos}</b> 的小友💖</div>
<div class="welcome-line">你的 IP 地址:<b class="ip-address">${ipDisplay}</b></div>
<div class="welcome-line">${getTimeGreeting()}</div>
<div class="welcome-line tip-line">Tip:<b>${greeting}🍂</b></div>
</div>
`;
// 🌓 适配 Butterfly 黑暗模式的样式
const addStyles = () => {
const style = document.createElement('style');
style.textContent = `
/* ===== 明亮模式(默认,保持原样式)===== */
#welcome-info {
user-select: none;
display: flex;
justify-content: center;
align-items: center;
min-height: 180px;
padding: 18px 24px;
margin-top: 10px;
border-radius: 12px;
/* 明亮模式固定颜色 */
background-color: #f8f9fa !important;
border: 1px solid #e0e0e0 !important;
line-height: 2;
font-size: 17px;
color: #333 !important;
}
.welcome-content {
text-align: center;
width: 100%;
}
.welcome-line {
margin: 6px 0;
}
.welcome-line b {
font-weight: 600;
/* 明亮模式固定主题色 */
color: #425aeff !important;
font-size: 17px;
}
.tip-line {
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed #e0e0e0;
}
/* IP 地址样式(明亮模式保持不变) */
.ip-address {
filter: blur(4px);
transition: filter 0.3s ease;
cursor: pointer;
padding: 3px 8px;
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
color: #333 !important;
}
.ip-address:hover {
filter: blur(0);
background: rgba(0, 0, 0, 0.1);
}
.loading-spinner {
width: 50px;
height: 50px;
border: 3px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top: 3px solid #425aeff;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
color: #ff6565;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
gap: 12px;
}
.error-icon {
font-size: 3rem;
}
#retry-button {
margin: 0 5px;
color: #425aeff;
cursor: pointer;
transition: transform 0.3s;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 17px;
}
#retry-button:hover {
transform: rotate(180deg);
}
/* 🌓 黑暗模式适配(Butterfly 主题) */
[data-theme="dark"] #welcome-info,
body.dark-mode #welcome-info,
.dark-theme #welcome-info {
/* 黑暗模式使用主题变量 */
background-color: var(--anzhiyu-background, #1c1c1c) !important;
border-color: var(--anzhiyu-card-border, #3a3a3a) !important;
color: var(--anzhiyu-fontcolor, #ccc) !important;
}
[data-theme="dark"] .welcome-line b,
body.dark-mode .welcome-line b,
.dark-theme .welcome-line b {
color: var(--anzhiyu-main, #5c82ff) !important;
}
[data-theme="dark"] .tip-line,
body.dark-mode .tip-line,
.dark-theme .tip-line {
border-top-color: var(--anzhiyu-card-border, #3a3a3a);
}
[data-theme="dark"] .ip-address,
body.dark-mode .ip-address,
.dark-theme .ip-address {
background: rgba(255, 255, 255, 0.08);
color: var(--anzhiyu-fontcolor, #ccc) !important;
}
[data-theme="dark"] .ip-address:hover,
body.dark-mode .ip-address:hover,
.dark-theme .ip-address:hover {
background: rgba(255, 255, 255, 0.12);
}
[data-theme="dark"] .loading-spinner,
body.dark-mode .loading-spinner,
.dark-theme .loading-spinner {
border-color: rgba(255, 255, 255, 0.1);
border-top-color: var(--anzhiyu-main, #5c82ff);
}
[data-theme="dark"] #retry-button,
body.dark-mode #retry-button,
.dark-theme #retry-button {
color: var(--anzhiyu-main, #5c82ff);
}
/* 移动端适配 */
@media (max-width: 768px) {
#welcome-info {
font-size: 15px;
padding: 14px 18px;
min-height: 160px;
}
.welcome-line b {
font-size: 15px;
}
#retry-button {
font-size: 15px;
}
}
`;
document.head.appendChild(style);
};
const showLoadingSpinner = () => {
const el = document.querySelector("#welcome-info");
if (el) el.innerHTML = '<div class="loading-spinner"></div>';
};
// 缓存管理
const IP_CACHE_KEY = 'ip_info_cache_v6';
const getFromCache = (key) => {
try {
const cached = localStorage.getItem(key);
if (!cached) return null;
const { data, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp > IP_CONFIG.CACHE_DURATION) {
localStorage.removeItem(key);
return null;
}
return data;
} catch { return null; }
};
const setToCache = (key, data) => {
try {
localStorage.setItem(key, JSON.stringify({ data, timestamp: Date.now() }));
} catch (e) { console.warn('缓存失败:', e); }
};
const showWelcome = ({ data, ip }) => {
const apiData = data?.data || data;
const userIp = apiData?.ip || ip;
if (!apiData?.address) {
console.error('❌ 地址数据为空:', apiData);
return showErrorMessage();
}
const { country, province, provinceKey, city } = parseAddress(apiData.address);
const ipDisplay = formatIpDisplay(userIp);
const pos = formatLocation(country, province, city);
const greeting = getGreeting(country, provinceKey);
const welcomeInfo = getWelcomeInfoElement();
if (!welcomeInfo) return;
welcomeInfo.style.display = 'block';
welcomeInfo.style.height = 'auto';
welcomeInfo.innerHTML = generateWelcomeMessage(pos, ipDisplay, greeting);
};
const fetchIpInfo = async () => {
showLoadingSpinner();
const cached = getFromCache(IP_CACHE_KEY);
if (cached) {
console.log('💾 使用缓存数据');
showWelcome(cached);
return;
}
try {
const data = await fetchIpData();
setToCache(IP_CACHE_KEY, data);
showWelcome(data);
} catch (error) {
console.error('❌ 获取 IP 信息失败:', error);
showErrorMessage();
}
};
const greetings = {
"中国": {
"北京": "北——京——欢迎你~~~",
"天津": "讲段相声,品盏茶汤",
"河北": "燕赵大地,慷慨悲歌",
"山西": "晋善晋美,表里山河",
"内蒙古": "天苍苍野茫茫,风吹草低见牛羊",
"辽宁": "辽沈大地,豪爽敞亮",
"吉林": "白山黑水,雾凇奇缘",
"黑龙江": "冰雪童话,北国风光",
"上海": "魔都闪耀,海纳百川",
"江苏": "江南水乡,温婉如画",
"浙江": "诗画浙江,人间天堂",
"安徽": "徽风皖韵,山水人文",
"福建": "山海福建,爱拼会赢",
"江西": "物华天宝,人杰地灵",
"山东": "孔孟之乡,好客山东",
"河南": "中原腹地,华夏之源",
"湖北": "千湖之省,荆楚风华",
"湖南": "潇湘山水,敢为人先",
"广东": "粤韵风华,食在广东",
"广西": "桂林山水甲天下",
"海南": "椰风海韵,天涯海角",
"四川": "天府之国,巴适得板",
"贵州": "山地公园,多彩贵州",
"云南": "彩云之南,心灵故乡",
"西藏": "圣洁西藏,心灵净土",
"陕西": "千年古都,常来长安",
"甘肃": "丝路明珠,壮美甘肃",
"青海": "大美青海,三江之源",
"宁夏": "塞上江南,神奇宁夏",
"新疆": "大美新疆,亚克西",
"台湾": "宝岛台湾,血脉相连",
"香港": "东方之珠,魅力香港",
"澳门": "莲花宝地,中西交融",
"其他": "欢迎来到我的小小世界✨"
},
"美国": "Hello! Welcome from the USA 🇺🇸",
"日本": "ようこそ!一緒に桜を見ましょう 🌸",
"俄罗斯": "Привет! 来自战斗民族的朋友 🇷🇺",
"法国": "Bonjour! C'est la vie 🇫🇷",
"德国": "Hallo! 严谨与浪漫的结合 🇩🇪",
"澳大利亚": "G'day! 欢迎来到南半球 🇦🇺",
"加拿大": "Hello! 枫叶之国欢迎你 🇨🇦",
"其他": "Welcome! 世界因你而精彩 🌍"
};
const getGreeting = (country, provinceKey) => {
const countryData = greetings[country];
if (!countryData) return greetings["其他"];
if (typeof countryData === 'string') return countryData;
return countryData[provinceKey] || countryData["其他"] || greetings["其他"];
};
const getTimeGreeting = () => {
const h = new Date().getHours();
if (h < 11) return "早上好🌤️ 一日之计在于晨";
if (h < 13) return "中午好☀️ 记得午休喔~";
if (h < 17) return "下午好🕞 饮茶先啦!";
if (h < 19) return "即将下班🚶‍♂️ 记得按时吃饭~";
return "晚上好🌙 夜生活嗨起来!";
};
const showErrorMessage = (message = '抱歉,无法获取信息') => {
const el = document.getElementById("welcome-info");
if (!el) return;
el.innerHTML = `
<div class="error-message">
<div class="error-icon">😕</div>
<p>${message}</p>
<p><span id="retry-button">🔄</span> 点击重试或检查网络</p>
</div>
`;
document.getElementById('retry-button')?.addEventListener('click', fetchIpInfo);
};
const isHomePage = () =>
window.location.pathname === '/' || window.location.pathname === '/index.html';
// 初始化
document.addEventListener('DOMContentLoaded', () => {
addStyles();
insertAnnouncementComponent();
document.addEventListener('pjax:complete', insertAnnouncementComponent);
});

引入Js#

_config.butterfly.yml主题配置文件下inject配置项中的bottom引入welcome.js

inject:
bottom:
- <script src="/js/card-welcome.js"></script>

引入卡片#

_config.butterfly.yml主题配置文件中引入<div id="welcome-info"></div>

card_announcement:
enable: true
content: <div id="welcome-info"></div>

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
添加来访者卡片
https://blog.yvyang.top/posts/33615/index.HTML/
作者
余漾
发布于
2026-03-14
许可协议
CC BY-NC-SA 4.0
相关文章 智能推荐
1
Butterfly:为博客添加微软Clarity数据统计
建站札记 这篇文章介绍了如何为博客添加微软Clarity数据统计工具,通过Hexo框架和Butterfly主题进行演示。首先需要注册微软账号并登录Clarity官网,然后新建项目并将网站添加到项目中。接下来,在`_config.butterfly.yml`主题配置文件中复制粘贴代码,按照缩进格式填写,最后保存后等待效果显现。
2
使用CloudFlare代理WebDav播放音乐
建站札记 这篇文章介绍了使用 CloudFlare Worker 实现 WebDav 服务的方法。文章中首先创建了一个名为 Workers 的目录,并在其中创建了两个文件:HelloWorld.worker.ts 和 proxyWorker.ts。这两个文件分别用于编写 HelloWorld 和代理音乐请求的代码。文章还提供了一些辅助函数,如 isOriginAllowed、getTestPage 和 debugInfo,以方便开发者进行调试和测试。
3
网站加速方案:阿里云免费版边缘安全加速ESA
建站札记 这篇文章介绍了如何通过阿里云免费版边缘安全加速ESA服务为个人博客网站加速。作者首先描述了自己无法使用国内CDN服务的情况,然后发现了阿里云的ESA服务,并决定尝试开通该服务。在开通过程中,作者详细解释了如何在控制台添加站点、设置DNS解析记录以及申请证书的具体步骤。
4
网站域名从blog.yvyang.fun迁移至blog.yvyang.top
建站札记 这篇文章主要讲述了作者从`yvyang.fun`这个域名迁移至`yvyang.top`的过程,以及将`yvyang.fun`重定向到`yvyang.top`的操作。文章还提到了续费价格的变化,以及作者对读者的祝福。
5
为网站添加并使用iconfont图标
建站札记 这篇文章介绍了如何在hexo-theme-butterfly主题中添加并使用Iconfont图标库。作者首先说明了在默认的FontAwesome图标库中存在的问题,如需要付费以及有时找不到特定图标的需求,随后转向引入Iconfont作为补充。Iconfont是由阿里妈妈MUX开发的一个矢量图标管理、交流平台。文章详细指导了注册账号和获取图标代码的过程,包括如何访问官网、选择图标加入购物车、添加到项目、更新配置文件等步骤。最后,文章总结了使用Iconfont图标服务的优势,并鼓励读者尝试这个工具以丰富网站内容。
随机文章 随机推荐

评论区

Profile Image of the Author
余漾
澄空碧山 绿水余漾
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
19
分类
2
标签
33
总字数
17,397
运行时长
0
总访问量
0
总访客数
0

文章目录