前言

这个是https://bbs.halo.run/d/6896-hao%E4%B8%BB%E9%A2%98%E5%B0%8F%E6%9D%BF%E6%8A%A5%E9%85%8D%E7%BD%AE看到的提问。

修改自:

https://github.com/BDTA-zky/GeoWelcome

示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>示例页面</title>
  <!-- <script type="module" src="config.js"></script> -->
  <script type="module" src="app.js"></script>
</head>
<body>
  <ip-info></ip-info>
</body>
</html>

效果

代码

app.js

/**
 * 使用示例:
 * <ip-info></ip-info>
 */

/** 
 * 引入配置文件,此处为默认路径,可在 <head> 或 <body> 标签中引入
 * 示例:<script type="module" src="config.js"></script>
 */
import config from './config.js';

class IPInfoComponent extends HTMLElement {
  constructor() {
    super();
    this.shadow = this.attachShadow({ mode: 'open' });
    this.ipInfoService = new IPInfoService(config);
    this.uiHandler = new UIHandler();
    this.render();
  }

  connectedCallback() {
    this.fetchIpInfo();
  }

  render() {
    const style = document.createElement('style');
    style.textContent = `
      :host {
        display: block;
        width: 100%;
      }
      .main-container {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: flex-start;
        width: 100%;
        box-sizing: border-box;
      }
      .welcome-message {
        background-color: var(--light-blue, #e7f3ff);
        border: 1px solid #b0d4f1;
        padding: 15px;
        border-radius: 8px;
        box-sizing: border-box;
      }
      .welcome-message p {
        margin: 8px 0;
        line-height: 1.5;
      }
      .ip-address {
        filter: blur(5px);
        transition: filter 0.3s ease;
      }
      .ip-address:hover {
        filter: blur(0);
      }
      .loading-spinner {
        width: 40px;
        height: 40px;
        border: 4px solid rgba(0, 0, 0, 0.1);
        border-radius: 50%;
        border-top: 4px solid var(--secondary-color, #4CAF50);
        animation: spin 1s linear infinite;
        margin-bottom: 10px;
      }
      .error-message {
        color: #ff6565;
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
      }
      .error-icon {
        font-size: 6vw;
        margin-bottom: 10px;
      }
      #error-message {
        font-size: 4vw;
        margin-bottom: 15px;
      }
      #retry-button {
        padding: 10px 20px;
        background-color: var(--secondary-color, #4CAF50);
        color: white;
        border: none;
        border-radius: 5px;
        font-size: 3vw;
        cursor: pointer;
      }
      #retry-button:hover {
        background-color: #388E3C;
      }
      @keyframes spin {
        0% { transform: rotate(0deg); }
        100% { transform: rotate(360deg); }
      }
      @media (min-width: 768px) {
        .error-icon { font-size: 2.5rem; }
        #error-message { font-size: 1.2rem; }
        #retry-button { font-size: 1rem; }
      }
    `;

    const container = document.createElement('div');
    container.className = 'main-container';
    container.innerHTML = `
      <div class="loading-spinner" id="loading-spinner"></div>
      <div class="welcome-message">
        <p>欢迎来自 <b id="visitor-location"></b> 的小友</p>
        <p>你当前距博主约 <b id="visitor-distance"></b> 公里!</p>
        <p>你的IP地址:<b class="ip-address" id="visitor-ip"></b></p>
        <p id="greeting-message"></p>
        <p>Tip:<b id="tip-message"></b></p>
      </div>
    `;

    this.shadow.append(style, container);
  }

  async fetchIpInfo() {
    try {
      const data = await this.ipInfoService.fetchIpData();
      this.uiHandler.showWelcome(
        data,
        (lng, lat) => this.ipInfoService.calculateDistance(lng, lat),
        this.shadow
      );
    } catch (error) {
      this.uiHandler.showErrorMessage(
        error instanceof Error ? error.message : '无法获取IP信息',
        this.shadow,
        () => this.fetchIpInfo()
      );
    }
  }
}

class IPInfoService {
  constructor(config) {
    this.config = { ...config };
  }

  async fetchIpData() {
    try {
      const response = await fetch(`${this.config.API_URL}${encodeURIComponent(this.config.API_KEY)}`);
      if (!response.ok) {
        throw new Error(`网络响应不正常: ${response.status} ${response.statusText}`);
      }
      return await response.json();
    } catch (error) {
      console.error('Fetch IP Data Error:', error);
      throw new Error('无法获取IP信息,请检查网络连接或API配置');
    }
  }

  calculateDistance(lng, lat) {
    const R = 6371; // 地球半径(公里)
    const rad = Math.PI / 180;
    const dLat = (lat - this.config.BLOG_LOCATION.lat) * rad;
    const dLon = (lng - this.config.BLOG_LOCATION.lng) * rad;
    const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
              Math.cos(this.config.BLOG_LOCATION.lat * rad) * Math.cos(lat * rad) *
              Math.sin(dLon / 2) * Math.sin(dLon / 2);
    return Math.round(R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)));
  }
}

class UIHandler {
  constructor() {
    this.greetings = config.GREETINGS;
  }

  showWelcome(data, distanceCalculator, shadowRoot) {
    const spinner = shadowRoot.getElementById('loading-spinner');
    if (spinner) spinner.style.display = 'none';

    if (!data || !data.data) return this.showErrorMessage('获取数据失败', shadowRoot);

    const { lng, lat, country, prov, city } = data.data;
    const dist = distanceCalculator(lng, lat);
    const ipDisplay = this.formatIpDisplay(data.ip);
    const pos = this.formatLocation(country, prov, city);

    shadowRoot.getElementById('visitor-location').textContent = pos;
    shadowRoot.getElementById('visitor-distance').textContent = dist.toString();
    shadowRoot.getElementById('visitor-ip').textContent = ipDisplay;
    shadowRoot.getElementById('greeting-message').textContent = this.getTimeGreeting();
    shadowRoot.getElementById('tip-message').textContent = this.getGreeting(country, prov, city);
  }

  showErrorMessage(message, shadowRoot, retryCallback) {
    const spinner = shadowRoot.getElementById('loading-spinner');
    if (spinner) spinner.style.display = 'none';

    const welcomeMessage = shadowRoot.querySelector('.welcome-message');
    if (welcomeMessage) {
      welcomeMessage.innerHTML = `
        <div class="error-message">
          <div class="error-icon"></div>
          <p id="error-message">${message}</p>
          <button id="retry-button">重试</button>
        </div>
      `;

      const retryButton = shadowRoot.getElementById('retry-button');
      if (retryButton) {
        retryButton.addEventListener('click', () => {
          if (spinner) spinner.style.display = 'block';
          retryCallback();
        });
      }
    }
  }

  getTimeGreeting() {
    const hour = new Date().getHours();
    for (const greeting of config.TIME_GREETINGS) {
      if (hour < greeting.hour) {
        return greeting.message;
      }
    }
    return config.TIME_GREETINGS[config.TIME_GREETINGS.length - 1].message;
  }

  getGreeting(country, province, city) {
    const countryGreeting = this.greetings[country] || this.greetings["其他"];
    if (typeof countryGreeting === 'string') return countryGreeting;
    const provinceGreeting = countryGreeting[province] || countryGreeting["其他"];
    return provinceGreeting[city] || provinceGreeting["其他"] || countryGreeting["其他"];
  }

  formatIpDisplay(ip) {
    return ip.includes(":") ? "复杂的IPv6地址" : ip;
  }

  formatLocation(country, prov, city) {
    return country ? (country === "中国" ? `${prov} ${city}` : country) : '神秘地区';
  }
}

customElements.define('ip-info', IPInfoComponent);

export default IPInfoComponent;

config.js

export default {
    API_KEY: 'key',
    API_URL: 'https://api.nsmao.net/api/ip/query?key=',
    BLOG_LOCATION: { lng: 155.27945, lat: 37.307755 },
    CACHE_DURATION: 1000 * 60 * 60,
    GREETINGS: {
      "中国": {
        "河南省": { "郑州市": "豫州之域,天地之中", "其他": "河南欢迎你!" },
        "其他": "中国欢迎你!"
      },
      "美国": "Let us live in peace!",
      "其他": "带我去你的国家逛逛吧"
    },
    TIME_GREETINGS: [
      { hour: 11, message: "早上好 ,一日之计在于晨" },
      { hour: 13, message: "中午好 ,记得午休喔~" },
      { hour: 17, message: "下午好 ,饮茶先啦!" },
      { hour: 19, message: "即将下班,记得按时吃饭~" },
      { hour: 24, message: "晚上好 ,夜生活嗨起来!" }
    ]
};