2.0
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
jQuery(document).ready(function($) {
|
||||
$('.view-location').on('click', function() {
|
||||
alert('经纬度:' + $(this).data('lat') + ', ' + $(this).data('lng'));
|
||||
});
|
||||
|
||||
$('.view-gpu').on('click', function() {
|
||||
alert('GPU 信息:\n' + $(this).data('gpu'));
|
||||
});
|
||||
|
||||
$('.view-ua').on('click', function() {
|
||||
alert('User Agent:\n' + $(this).data('ua'));
|
||||
});
|
||||
|
||||
$('.view-uach').on('click', function() {
|
||||
alert('User Agent Client Hints:\n' + $(this).data('uach'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
.hardware-tracker-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.hardware-tracker-table th,
|
||||
.hardware-tracker-table td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 8px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.hardware-tracker-table td .alert {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
button {
|
||||
margin-top: 4px;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 9999;
|
||||
}
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
margin: 10% auto;
|
||||
padding: 20px;
|
||||
width: 400px;
|
||||
position: relative;
|
||||
}
|
||||
.modal .close {
|
||||
position: absolute;
|
||||
top: 10px; right: 10px;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.time-group div { margin: 2px 0; }
|
||||
.geo-info div { margin: 3px 0; }
|
||||
.timezone-warning { background: #fff3e0; padding: 5px; border-radius: 3px; }
|
||||
.no-data { text-align: center; color: #666; padding: 20px !important; }
|
||||
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
class Hardware_Tracker_UI {
|
||||
public function __construct() {
|
||||
add_action('admin_menu', [$this, 'add_menu_page']);
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
|
||||
}
|
||||
|
||||
public function add_menu_page() {
|
||||
add_menu_page(
|
||||
'访客追踪',
|
||||
'访客追踪',
|
||||
'manage_options',
|
||||
'hardware-tracker',
|
||||
[$this, 'render_dashboard_page'],
|
||||
'dashicons-visibility',
|
||||
25
|
||||
);
|
||||
}
|
||||
|
||||
public function enqueue_assets($hook) {
|
||||
if (isset($_GET['page']) && $_GET['page'] === 'hardware-tracker') {
|
||||
wp_enqueue_script('hardware-tracker-script',
|
||||
plugin_dir_url(__FILE__) . 'assets/script.js',
|
||||
['jquery'], null, true
|
||||
);
|
||||
wp_enqueue_style('hardware-tracker-style',
|
||||
plugin_dir_url(__FILE__) . 'assets/style.css'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function render_dashboard_page() {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'hardware_visitors';
|
||||
|
||||
// 分页参数
|
||||
$per_page = 20;
|
||||
$current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
|
||||
$offset = ($current_page - 1) * $per_page;
|
||||
|
||||
// 搜索逻辑
|
||||
$search = isset($_GET['s']) ? sanitize_text_field($_GET['s']) : '';
|
||||
$where_clause = '1=1';
|
||||
$query_params = [];
|
||||
|
||||
if (!empty($search)) {
|
||||
$like = '%' . $wpdb->esc_like($search) . '%';
|
||||
$where_clause .= $wpdb->prepare(" AND (ip LIKE %s OR os_name LIKE %s OR browser_name LIKE %s)", $like, $like, $like);
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
$total_items = $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE $where_clause");
|
||||
$results = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM $table
|
||||
WHERE $where_clause
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %d OFFSET %d",
|
||||
$per_page, $offset
|
||||
));
|
||||
|
||||
// 界面输出
|
||||
echo '<div class="wrap"><h1>访客追踪</h1>';
|
||||
|
||||
// 搜索框
|
||||
echo '<form method="get" class="search-form">
|
||||
<input type="hidden" name="page" value="hardware-tracker">
|
||||
<div class="search-box">
|
||||
<input type="search" name="s" value="' . esc_attr($search) . '" placeholder="搜索 IP/浏览器/系统">
|
||||
<button type="submit" class="button">搜索</button>
|
||||
</div>
|
||||
</form>';
|
||||
|
||||
// 数据表格
|
||||
echo '<table class="widefat striped hardware-tracker-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>操作系统</th>
|
||||
<th>浏览器</th>
|
||||
<th>IP地址</th>
|
||||
<th>地理位置</th>
|
||||
<th>硬件配置</th>
|
||||
<th>用户代理</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>';
|
||||
|
||||
if ($results) {
|
||||
foreach ($results as $row) {
|
||||
// 服务器时间(上海时区)
|
||||
$server_time = date('Y-m-d H:i:s', strtotime($row->created_at));
|
||||
|
||||
// ============== 用户时间(浏览器时区) ==============
|
||||
$user_time = '未知';
|
||||
$user_timezone = null;
|
||||
if (!empty($row->timezone)) {
|
||||
try {
|
||||
$user_timezone = new DateTimeZone($row->timezone);
|
||||
$user_time = (new DateTime($row->created_at, new DateTimeZone('Asia/Shanghai')))
|
||||
->setTimezone($user_timezone)
|
||||
->format('Y-m-d H:i:s');
|
||||
} catch (Exception $e) {
|
||||
$user_time = '时区无效';
|
||||
}
|
||||
}
|
||||
|
||||
// ============== IP地区时间(GeoIP时区) ==============
|
||||
$ip_time = '未知';
|
||||
$geo_timezone = $row->geo_timezone ?? 'unknown';
|
||||
if (!empty($geo_timezone) && $geo_timezone !== 'unknown') {
|
||||
try {
|
||||
$ip_timezone = new DateTimeZone($geo_timezone);
|
||||
$ip_time = (new DateTime($row->created_at, new DateTimeZone('Asia/Shanghai')))
|
||||
->setTimezone($ip_timezone)
|
||||
->format('Y-m-d H:i:s');
|
||||
} catch (Exception $e) {
|
||||
$ip_time = '时区无效: ' . esc_html($geo_timezone);
|
||||
}
|
||||
}
|
||||
|
||||
// ============== 时区不一致警告 ==============
|
||||
$timezone_warning = '';
|
||||
if ($user_timezone && $geo_timezone !== 'unknown') {
|
||||
$time_diff = $this->calculate_timezone_diff($user_timezone, new DateTimeZone($geo_timezone));
|
||||
if ($time_diff !== 0) {
|
||||
$timezone_warning = '<div class="timezone-warning" style="color:#d63638;">
|
||||
⚠️ 时区偏移: ' . $time_diff . ' 小时
|
||||
</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// 输出表格行
|
||||
echo '<tr>';
|
||||
echo '<td>
|
||||
<div class="time-group">
|
||||
<div class="time-server">🕒 服务器: ' . esc_html($server_time) . '</div>
|
||||
<div class="time-user">👤 用户: ' . esc_html($user_time) . '</div>
|
||||
<div class="time-ip">🌍 IP地区: ' . esc_html($ip_time) . '</div>
|
||||
' . $timezone_warning . '
|
||||
</div>
|
||||
</td>';
|
||||
echo '<td>' . esc_html("{$row->os_name} {$row->os_version}") . '</td>';
|
||||
echo '<td>' . esc_html("{$row->browser_name} {$row->browser_version}") . '</td>';
|
||||
echo '<td>' . esc_html($row->ip) . '</td>';
|
||||
echo '<td>
|
||||
<div class="geo-info">
|
||||
<div class="geo-country">🏳️ ' . esc_html($row->country) . '</div>
|
||||
<div class="geo-region">📍 ' . esc_html("{$row->region} · {$row->city}") . '</div>
|
||||
<div class="geo-timezone">⏰ ' . esc_html($geo_timezone) . '</div>
|
||||
<button class="button view-location"
|
||||
data-lat="' . esc_attr($row->latitude) . '"
|
||||
data-lng="' . esc_attr($row->longitude) . '">
|
||||
查看经纬度
|
||||
</button>
|
||||
</div>
|
||||
</td>';
|
||||
echo '<td>
|
||||
<div class="hardware-info">
|
||||
<div class="cpu-info">💻 ' . esc_html("{$row->cpu_arch} · {$row->cpu_cores}核") . '</div>
|
||||
<button class="button view-gpu"
|
||||
data-gpu="' . esc_attr("{$row->gpu_vendor} - {$row->gpu_model}") . '">
|
||||
GPU详情
|
||||
</button>
|
||||
</div>
|
||||
</td>';
|
||||
echo '<td>
|
||||
<div class="ua-info">
|
||||
<button class="button view-ua"
|
||||
data-ua="' . esc_attr($row->user_agent) . '">
|
||||
UA
|
||||
</button>
|
||||
<button class="button view-uach"
|
||||
data-uach="' . esc_attr($row->ua_ch) . '">
|
||||
UA-CH
|
||||
</button>
|
||||
</div>
|
||||
</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
} else {
|
||||
echo '<tr><td colspan="7" class="no-data">😢 暂无追踪数据</td></tr>';
|
||||
}
|
||||
|
||||
echo '</tbody></table>';
|
||||
|
||||
// 分页导航
|
||||
if ($total_items > $per_page) {
|
||||
echo '<div class="tablenav bottom">
|
||||
<div class="tablenav-pages">
|
||||
' . paginate_links([
|
||||
'base' => add_query_arg('paged', '%#%'),
|
||||
'format' => '',
|
||||
'current' => $current_page,
|
||||
'total' => ceil($total_items / $per_page)
|
||||
]) . '
|
||||
</div>
|
||||
</div>';
|
||||
}
|
||||
|
||||
echo '</div>'; // 结束 .wrap
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个时区的小时差
|
||||
*/
|
||||
private function calculate_timezone_diff(DateTimeZone $tz1, DateTimeZone $tz2): int {
|
||||
$date = new DateTime('now', $tz1);
|
||||
$offset1 = $tz1->getOffset($date);
|
||||
$offset2 = $tz2->getOffset($date);
|
||||
return (int) round(($offset2 - $offset1) / 3600);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
class Hardware_Tracker_GeoIP_Resolver {
|
||||
private $reader;
|
||||
private $db_path;
|
||||
|
||||
public function __construct() {
|
||||
$this->db_path = plugin_dir_path(dirname(__FILE__, 2)) . 'data/GeoLite2-City.mmdb';
|
||||
$this->initialize_reader();
|
||||
}
|
||||
|
||||
private function initialize_reader() {
|
||||
try {
|
||||
if (file_exists($this->db_path)) {
|
||||
$this->reader = new MaxMind\Db\Reader($this->db_path);
|
||||
error_log('[硬件追踪] GeoIP数据库加载成功');
|
||||
} else {
|
||||
error_log('[硬件追踪] 数据库文件未找到:' . $this->db_path);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log('[硬件追踪] GeoIP初始化失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function resolve_ip($ip) {
|
||||
if (!$this->reader || !filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$record = $this->reader->get($ip);
|
||||
return $this->parse_record($record);
|
||||
} catch (Exception $e) {
|
||||
error_log('[硬件追踪] IP解析失败:' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function parse_record($record) {
|
||||
return [
|
||||
'country' => $record['country']['names']['en'] ?? 'unknown',
|
||||
'region' => $record['subdivisions'][0]['names']['en'] ?? 'unknown',
|
||||
'city' => $record['city']['names']['en'] ?? 'unknown',
|
||||
'district' => $record['subdivisions'][1]['names']['en'] ?? 'unknown',
|
||||
'latitude' => $record['location']['latitude'] ?? 0,
|
||||
'longitude' => $record['location']['longitude'] ?? 0,
|
||||
'geo_timezone' => $record['location']['time_zone'] ?? 'unknown'
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user