custom.css
· 674 B · CSS
Brut
/* ===== 隐私政策页面信息加载动画 ===== */
.privacy-cell-loading {
position: relative;
color: transparent !important;
background: linear-gradient(
90deg,
var(--anzhiyu-secondbg) 25%,
var(--anzhiyu-gray-op) 50%,
var(--anzhiyu-secondbg) 75%
);
background-size: 200% 100%;
animation: privacy-shimmer 1.5s ease-in-out infinite;
border-radius: 4px;
min-width: 80px;
display: inline-block;
}
@keyframes privacy-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
#article-container table td {
transition: background-color 0.3s ease, color 0.3s ease;
}
| 1 | /* ===== 隐私政策页面信息加载动画 ===== */ |
| 2 | .privacy-cell-loading { |
| 3 | position: relative; |
| 4 | color: transparent !important; |
| 5 | background: linear-gradient( |
| 6 | 90deg, |
| 7 | var(--anzhiyu-secondbg) 25%, |
| 8 | var(--anzhiyu-gray-op) 50%, |
| 9 | var(--anzhiyu-secondbg) 75% |
| 10 | ); |
| 11 | background-size: 200% 100%; |
| 12 | animation: privacy-shimmer 1.5s ease-in-out infinite; |
| 13 | border-radius: 4px; |
| 14 | min-width: 80px; |
| 15 | display: inline-block; |
| 16 | } |
| 17 | |
| 18 | @keyframes privacy-shimmer { |
| 19 | 0% { background-position: 200% 0; } |
| 20 | 100% { background-position: -200% 0; } |
| 21 | } |
| 22 | |
| 23 | #article-container table td { |
| 24 | transition: background-color 0.3s ease, color 0.3s ease; |
| 25 | } |
| 类型 | 信息 |
|---|---|
| 网络信息 | |
| IP地址 | 获取中... |
| 国家 | 获取中... |
| 省份 | 获取中... |
| 城市 | 获取中... |
| 运营商 | 获取中... |
| 设备信息 | |
| 操作系统 | 获取中... |
| 浏览器 | 获取中... |
privacy-info.js
· 6.1 KiB · JavaScript
Brut
/**
* 隐私政策页面 - 动态展示访客信息
* 使用腾讯地图IP定位API + ipapi.co获取网络信息
* 使用navigator.userAgent解析设备信息
*/
(function () {
'use strict';
const CONFIG = {
TENCENT_KEY: 'FXNBZ-T5DCB-ABWUU-NXPTT-4AXGH-IUBAQ',
CACHE_KEY: 'privacy_ip_cache',
CACHE_TTL: 1000 * 60 * 60, // 1小时
};
// ========== 页面检测 ==========
function isPrivacyPage() {
const p = window.location.pathname;
return p === '/privacy/' || p === '/privacy' || p === '/privacy/index.html';
}
// ========== 缓存 ==========
function getFromCache() {
try {
const raw = localStorage.getItem(CONFIG.CACHE_KEY);
if (!raw) return null;
const { data, ts } = JSON.parse(raw);
if (Date.now() - ts > CONFIG.CACHE_TTL) {
localStorage.removeItem(CONFIG.CACHE_KEY);
return null;
}
return data;
} catch {
return null;
}
}
function setCache(data) {
try {
localStorage.setItem(CONFIG.CACHE_KEY, JSON.stringify({ data, ts: Date.now() }));
} catch { /* quota exceeded, ignore */ }
}
// ========== 腾讯地图IP定位 (JSONP) ==========
function fetchTencentIP() {
return new Promise((resolve, reject) => {
const cb = `_privacy_qqmap_${Date.now()}`;
const script = document.createElement('script');
script.src = `https://apis.map.qq.com/ws/location/v1/ip?key=${encodeURIComponent(CONFIG.TENCENT_KEY)}&output=jsonp&callback=${cb}`;
window[cb] = (res) => {
cleanup();
if (res.status !== 0) return reject(new Error(res.message));
const { ad_info } = res.result;
resolve({
ip: res.result.ip || '',
country: ad_info.nation || '',
province: ad_info.province || '',
city: ad_info.city || '',
});
};
script.onerror = () => { cleanup(); reject(new Error('JSONP request failed')); };
function cleanup() {
document.head.removeChild(script);
delete window[cb];
}
document.head.appendChild(script);
});
}
// ========== 运营商检测 (ipapi.co, HTTPS) ==========
function fetchISP() {
return fetch('https://ipapi.co/json/')
.then(r => r.json())
.then(d => d.org || '获取失败');
}
// ========== UA解析 ==========
function parseUserAgent() {
const ua = navigator.userAgent;
return { os: detectOS(ua), browser: detectBrowser(ua) };
}
function detectOS(ua) {
if (/Windows NT 10\.0/.test(ua)) return 'Windows 10/11';
if (/Windows NT 6\.3/.test(ua)) return 'Windows 8.1';
if (/Windows NT 6\.2/.test(ua)) return 'Windows 8';
if (/Windows NT 6\.1/.test(ua)) return 'Windows 7';
if (/Windows/.test(ua)) return 'Windows';
const macMatch = ua.match(/Mac OS X ([\d_]+)/);
if (macMatch) return 'macOS ' + macMatch[1].replace(/_/g, '.');
const androidMatch = ua.match(/Android ([\d.]+)/);
if (androidMatch) return 'Android ' + androidMatch[1];
const iosMatch = ua.match(/OS ([\d_]+)/);
if (/iPhone|iPad|iPod/.test(ua) && iosMatch) return 'iOS ' + iosMatch[1].replace(/_/g, '.');
if (/Linux/.test(ua)) return 'Linux';
return '未知';
}
function detectBrowser(ua) {
if (/Edg\//.test(ua)) return 'Edge ' + (ua.match(/Edg\/([\d.]+)/) || [])[1];
if (/OPR\//.test(ua)) return 'Opera ' + (ua.match(/OPR\/([\d.]+)/) || [])[1];
if (/Chrome\//.test(ua)) return 'Chrome ' + (ua.match(/Chrome\/([\d.]+)/) || [])[1];
if (/Firefox\//.test(ua)) return 'Firefox ' + (ua.match(/Firefox\/([\d.]+)/) || [])[1];
if (/Safari\//.test(ua)) return 'Safari ' + (ua.match(/Version\/([\d.]+)/) || [])[1];
return '未知';
}
// ========== 表格操作 ==========
function findPrivacyTable() {
const tables = document.querySelectorAll('#article-container table');
for (const table of tables) {
const rows = table.querySelectorAll('tbody tr');
for (const row of rows) {
const firstCell = row.querySelector('td:first-child');
if (firstCell && firstCell.textContent.trim() === 'IP地址') {
return table;
}
}
}
return null;
}
function getCellByLabel(table, label) {
const rows = table.querySelectorAll('tbody tr');
for (const row of rows) {
const firstCell = row.querySelector('td:first-child');
if (firstCell && firstCell.textContent.trim() === label) {
return row.querySelector('td:nth-child(2)');
}
}
return null;
}
function fillCell(table, label, value) {
const cell = getCellByLabel(table, label);
if (!cell) return;
cell.classList.remove('privacy-cell-loading');
cell.textContent = value || '获取失败';
}
function addLoadingClass(table) {
const labels = ['IP地址', '国家', '省份', '城市', '运营商', '操作系统', '浏览器'];
labels.forEach(label => {
const cell = getCellByLabel(table, label);
if (cell) cell.classList.add('privacy-cell-loading');
});
}
// ========== 主逻辑 ==========
function init() {
if (!isPrivacyPage()) return;
const table = findPrivacyTable();
if (!table) return;
addLoadingClass(table);
const cached = getFromCache();
if (cached) {
fillAll(table, cached);
return;
}
// 并行获取数据
Promise.allSettled([fetchTencentIP(), fetchISP()]).then(([tencentRes, ispRes]) => {
const tencent = tencentRes.status === 'fulfilled' ? tencentRes.value : {};
const isp = ispRes.status === 'fulfilled' ? ispRes.value : '获取失败';
const ua = parseUserAgent();
const data = {
ip: tencent.ip || '获取失败',
country: tencent.country || '获取失败',
province: tencent.province || '获取失败',
city: tencent.city || '获取失败',
isp: isp,
os: ua.os,
browser: ua.browser,
};
fillAll(table, data);
setCache(data);
});
}
function fillAll(table, data) {
fillCell(table, 'IP地址', data.ip);
fillCell(table, '国家', data.country);
fillCell(table, '省份', data.province);
fillCell(table, '城市', data.city);
fillCell(table, '运营商', data.isp);
fillCell(table, '操作系统', data.os);
fillCell(table, '浏览器', data.browser);
}
// ========== 初始化(含Pjax兼容) ==========
function safeInit() {
// 延迟执行,等待主题的 addTableWrap() 完成
setTimeout(init, 100);
}
document.addEventListener('DOMContentLoaded', safeInit);
document.addEventListener('pjax:complete', safeInit);
})();
| 1 | /** |
| 2 | * 隐私政策页面 - 动态展示访客信息 |
| 3 | * 使用腾讯地图IP定位API + ipapi.co获取网络信息 |
| 4 | * 使用navigator.userAgent解析设备信息 |
| 5 | */ |
| 6 | (function () { |
| 7 | 'use strict'; |
| 8 | |
| 9 | const CONFIG = { |
| 10 | TENCENT_KEY: 'FXNBZ-T5DCB-ABWUU-NXPTT-4AXGH-IUBAQ', |
| 11 | CACHE_KEY: 'privacy_ip_cache', |
| 12 | CACHE_TTL: 1000 * 60 * 60, // 1小时 |
| 13 | }; |
| 14 | |
| 15 | // ========== 页面检测 ========== |
| 16 | function isPrivacyPage() { |
| 17 | const p = window.location.pathname; |
| 18 | return p === '/privacy/' || p === '/privacy' || p === '/privacy/index.html'; |
| 19 | } |
| 20 | |
| 21 | // ========== 缓存 ========== |
| 22 | function getFromCache() { |
| 23 | try { |
| 24 | const raw = localStorage.getItem(CONFIG.CACHE_KEY); |
| 25 | if (!raw) return null; |
| 26 | const { data, ts } = JSON.parse(raw); |
| 27 | if (Date.now() - ts > CONFIG.CACHE_TTL) { |
| 28 | localStorage.removeItem(CONFIG.CACHE_KEY); |
| 29 | return null; |
| 30 | } |
| 31 | return data; |
| 32 | } catch { |
| 33 | return null; |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | function setCache(data) { |
| 38 | try { |
| 39 | localStorage.setItem(CONFIG.CACHE_KEY, JSON.stringify({ data, ts: Date.now() })); |
| 40 | } catch { /* quota exceeded, ignore */ } |
| 41 | } |
| 42 | |
| 43 | // ========== 腾讯地图IP定位 (JSONP) ========== |
| 44 | function fetchTencentIP() { |
| 45 | return new Promise((resolve, reject) => { |
| 46 | const cb = `_privacy_qqmap_${Date.now()}`; |
| 47 | const script = document.createElement('script'); |
| 48 | script.src = `https://apis.map.qq.com/ws/location/v1/ip?key=${encodeURIComponent(CONFIG.TENCENT_KEY)}&output=jsonp&callback=${cb}`; |
| 49 | |
| 50 | window[cb] = (res) => { |
| 51 | cleanup(); |
| 52 | if (res.status !== 0) return reject(new Error(res.message)); |
| 53 | const { ad_info } = res.result; |
| 54 | resolve({ |
| 55 | ip: res.result.ip || '', |
| 56 | country: ad_info.nation || '', |
| 57 | province: ad_info.province || '', |
| 58 | city: ad_info.city || '', |
| 59 | }); |
| 60 | }; |
| 61 | |
| 62 | script.onerror = () => { cleanup(); reject(new Error('JSONP request failed')); }; |
| 63 | |
| 64 | function cleanup() { |
| 65 | document.head.removeChild(script); |
| 66 | delete window[cb]; |
| 67 | } |
| 68 | |
| 69 | document.head.appendChild(script); |
| 70 | }); |
| 71 | } |
| 72 | |
| 73 | // ========== 运营商检测 (ipapi.co, HTTPS) ========== |
| 74 | function fetchISP() { |
| 75 | return fetch('https://ipapi.co/json/') |
| 76 | .then(r => r.json()) |
| 77 | .then(d => d.org || '获取失败'); |
| 78 | } |
| 79 | |
| 80 | // ========== UA解析 ========== |
| 81 | function parseUserAgent() { |
| 82 | const ua = navigator.userAgent; |
| 83 | return { os: detectOS(ua), browser: detectBrowser(ua) }; |
| 84 | } |
| 85 | |
| 86 | function detectOS(ua) { |
| 87 | if (/Windows NT 10\.0/.test(ua)) return 'Windows 10/11'; |
| 88 | if (/Windows NT 6\.3/.test(ua)) return 'Windows 8.1'; |
| 89 | if (/Windows NT 6\.2/.test(ua)) return 'Windows 8'; |
| 90 | if (/Windows NT 6\.1/.test(ua)) return 'Windows 7'; |
| 91 | if (/Windows/.test(ua)) return 'Windows'; |
| 92 | |
| 93 | const macMatch = ua.match(/Mac OS X ([\d_]+)/); |
| 94 | if (macMatch) return 'macOS ' + macMatch[1].replace(/_/g, '.'); |
| 95 | |
| 96 | const androidMatch = ua.match(/Android ([\d.]+)/); |
| 97 | if (androidMatch) return 'Android ' + androidMatch[1]; |
| 98 | |
| 99 | const iosMatch = ua.match(/OS ([\d_]+)/); |
| 100 | if (/iPhone|iPad|iPod/.test(ua) && iosMatch) return 'iOS ' + iosMatch[1].replace(/_/g, '.'); |
| 101 | |
| 102 | if (/Linux/.test(ua)) return 'Linux'; |
| 103 | return '未知'; |
| 104 | } |
| 105 | |
| 106 | function detectBrowser(ua) { |
| 107 | if (/Edg\//.test(ua)) return 'Edge ' + (ua.match(/Edg\/([\d.]+)/) || [])[1]; |
| 108 | if (/OPR\//.test(ua)) return 'Opera ' + (ua.match(/OPR\/([\d.]+)/) || [])[1]; |
| 109 | if (/Chrome\//.test(ua)) return 'Chrome ' + (ua.match(/Chrome\/([\d.]+)/) || [])[1]; |
| 110 | if (/Firefox\//.test(ua)) return 'Firefox ' + (ua.match(/Firefox\/([\d.]+)/) || [])[1]; |
| 111 | if (/Safari\//.test(ua)) return 'Safari ' + (ua.match(/Version\/([\d.]+)/) || [])[1]; |
| 112 | return '未知'; |
| 113 | } |
| 114 | |
| 115 | // ========== 表格操作 ========== |
| 116 | function findPrivacyTable() { |
| 117 | const tables = document.querySelectorAll('#article-container table'); |
| 118 | for (const table of tables) { |
| 119 | const rows = table.querySelectorAll('tbody tr'); |
| 120 | for (const row of rows) { |
| 121 | const firstCell = row.querySelector('td:first-child'); |
| 122 | if (firstCell && firstCell.textContent.trim() === 'IP地址') { |
| 123 | return table; |
| 124 | } |
| 125 | } |
| 126 | } |
| 127 | return null; |
| 128 | } |
| 129 | |
| 130 | function getCellByLabel(table, label) { |
| 131 | const rows = table.querySelectorAll('tbody tr'); |
| 132 | for (const row of rows) { |
| 133 | const firstCell = row.querySelector('td:first-child'); |
| 134 | if (firstCell && firstCell.textContent.trim() === label) { |
| 135 | return row.querySelector('td:nth-child(2)'); |
| 136 | } |
| 137 | } |
| 138 | return null; |
| 139 | } |
| 140 | |
| 141 | function fillCell(table, label, value) { |
| 142 | const cell = getCellByLabel(table, label); |
| 143 | if (!cell) return; |
| 144 | cell.classList.remove('privacy-cell-loading'); |
| 145 | cell.textContent = value || '获取失败'; |
| 146 | } |
| 147 | |
| 148 | function addLoadingClass(table) { |
| 149 | const labels = ['IP地址', '国家', '省份', '城市', '运营商', '操作系统', '浏览器']; |
| 150 | labels.forEach(label => { |
| 151 | const cell = getCellByLabel(table, label); |
| 152 | if (cell) cell.classList.add('privacy-cell-loading'); |
| 153 | }); |
| 154 | } |
| 155 | |
| 156 | // ========== 主逻辑 ========== |
| 157 | function init() { |
| 158 | if (!isPrivacyPage()) return; |
| 159 | |
| 160 | const table = findPrivacyTable(); |
| 161 | if (!table) return; |
| 162 | |
| 163 | addLoadingClass(table); |
| 164 | |
| 165 | const cached = getFromCache(); |
| 166 | if (cached) { |
| 167 | fillAll(table, cached); |
| 168 | return; |
| 169 | } |
| 170 | |
| 171 | // 并行获取数据 |
| 172 | Promise.allSettled([fetchTencentIP(), fetchISP()]).then(([tencentRes, ispRes]) => { |
| 173 | const tencent = tencentRes.status === 'fulfilled' ? tencentRes.value : {}; |
| 174 | const isp = ispRes.status === 'fulfilled' ? ispRes.value : '获取失败'; |
| 175 | const ua = parseUserAgent(); |
| 176 | |
| 177 | const data = { |
| 178 | ip: tencent.ip || '获取失败', |
| 179 | country: tencent.country || '获取失败', |
| 180 | province: tencent.province || '获取失败', |
| 181 | city: tencent.city || '获取失败', |
| 182 | isp: isp, |
| 183 | os: ua.os, |
| 184 | browser: ua.browser, |
| 185 | }; |
| 186 | |
| 187 | fillAll(table, data); |
| 188 | setCache(data); |
| 189 | }); |
| 190 | } |
| 191 | |
| 192 | function fillAll(table, data) { |
| 193 | fillCell(table, 'IP地址', data.ip); |
| 194 | fillCell(table, '国家', data.country); |
| 195 | fillCell(table, '省份', data.province); |
| 196 | fillCell(table, '城市', data.city); |
| 197 | fillCell(table, '运营商', data.isp); |
| 198 | fillCell(table, '操作系统', data.os); |
| 199 | fillCell(table, '浏览器', data.browser); |
| 200 | } |
| 201 | |
| 202 | // ========== 初始化(含Pjax兼容) ========== |
| 203 | function safeInit() { |
| 204 | // 延迟执行,等待主题的 addTableWrap() 完成 |
| 205 | setTimeout(init, 100); |
| 206 | } |
| 207 | |
| 208 | document.addEventListener('DOMContentLoaded', safeInit); |
| 209 | document.addEventListener('pjax:complete', safeInit); |
| 210 | })(); |