YvYang's Blog

添加来访者卡片

这篇文章介绍了在Hexo博客中使用小小API替代失效的Nice猫API来实现访客卡片功能的方法。文章首先说明了更换API的背景原因,随后详细讲解了创建welcome.js文件的步骤,包括IP数据获取、地址解析、缓存管理、欢迎消息生成等核心代码逻辑,并提供了完整的JavaScript代码示例。最后指导用户如何在主题配置文件中引入JS文件和配置卡片内容,使博客能够根据访客的地理位置和IP地址显示个性化的欢迎信息,提升网站交互体验。

1.9千字 预计阅读 7 分钟 0 次浏览

前言

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

教程部分

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

创建js文件

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

JavaScript
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

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

引入卡片

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

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

文章作者: 余漾

文章链接: https://blog.yvyang.top/posts/33615/index.HTML

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 YvYang's Blog

评论加载中…