前言
这个是
修改自:
示例
<!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: "晚上好 ,夜生活嗨起来!" }
]
};