Files
CLOAK/table.php
T
2026-06-16 04:58:56 +08:00

1056 lines
39 KiB
PHP
Executable File

<?php
require_once("cong.php");
session_start();
if(empty($_SESSION['password']) || $_SESSION['password'] != LOGIN_PASSWORD) {
unset($_SESSION['password']);
unset($_SESSION['check_result']);
echo("<script language=javascript>alert('密码错误!');window.location='ip_check_edit.php'; </script>");
//die("输入的密码不正确,请重新输入。");
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>访问判断日志</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="//cdn.staticfile.org/layui/2.9.2/css/layui.css" rel="stylesheet">
<style>
:root {
--log-primary: #16baaa;
--log-primary-dark: #0e8d81;
--log-bg: #f0f2f5;
--log-card: #ffffff;
--log-border: #e8ecf1;
--log-header-bg: #f4f7fb;
--log-text: #2c3e50;
--log-muted: #8c98a4;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--log-bg);
color: var(--log-text);
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
}
.log-page {
max-width: 1680px;
margin: 0 auto;
padding: 24px 28px 40px;
}
.log-page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 12px;
}
.log-page-header h1 {
margin: 0;
font-size: 1.35rem;
font-weight: 600;
color: var(--log-text);
}
.log-page-header p {
margin: 4px 0 0;
font-size: .85rem;
color: var(--log-muted);
}
.log-card {
background: var(--log-card);
border: 1px solid var(--log-border);
border-radius: 10px;
box-shadow: 0 2px 12px rgba(15, 23, 42, .04);
margin-bottom: 16px;
overflow: hidden;
}
.log-card-body { padding: 18px 20px; }
.log-filter-grid .layui-col-md3 { margin-bottom: 12px; }
.log-filter-grid .layui-input,
.log-filter-grid .layui-select-title input {
border-radius: 6px;
border-color: var(--log-border);
}
.log-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
padding: 14px 20px;
border-top: 1px solid var(--log-border);
background: #fafbfc;
}
.log-actions .layui-btn { border-radius: 6px; }
.log-actions .layui-btn-primary { background: #fff; }
.log-count {
margin-left: auto;
font-size: .88rem;
color: var(--log-muted);
}
.log-count strong {
color: var(--log-primary-dark);
font-size: 1rem;
margin: 0 4px;
}
#btn-clear { background: #e74c3c !important; border-color: #e74c3c !important; }
.log-table-wrap { padding: 0; }
.log-table-wrap .layui-table-view {
border: none;
border-radius: 0 0 10px 10px;
margin: 0;
}
/* Sticky header */
.log-table-wrap .layui-table-header {
position: sticky;
top: 0;
z-index: 20;
border-radius: 0;
}
.log-table-wrap .layui-table-header .layui-table {
background: linear-gradient(180deg, #eef3f9 0%, #e4ebf5 100%);
}
.log-table-wrap .layui-table-header th {
color: #3d5167 !important;
font-weight: 600 !important;
font-size: 12.5px;
border-color: #d8e0ea !important;
padding: 11px 10px !important;
}
/* Zebra + hover */
.log-table-wrap .layui-table-body tr:nth-child(even) td {
background-color: #fafbfc;
}
.log-table-wrap .layui-table-body tr:hover td {
background-color: #eef6ff !important;
}
.log-table-wrap .layui-table-body td {
border-color: #eef1f5 !important;
padding: 9px 10px !important;
font-size: 12.5px;
line-height: 1.45;
transition: background-color .15s ease;
}
.log-table-wrap .layui-table-page {
border-top: 1px solid var(--log-border);
padding: 12px 16px;
background: #fafbfc;
}
/* Badges */
.cloak-badge {
display: inline-block;
padding: 2px 9px;
border-radius: 999px;
font-size: 11.5px;
font-weight: 600;
line-height: 1.5;
white-space: nowrap;
letter-spacing: .02em;
}
.badge-success { background: #d4edda; color: #155724; }
.badge-error { background: #f8d7da; color: #721c24; }
.badge-warning { background: #fff3cd; color: #856404; }
.badge-info { background: #d1ecf1; color: #0c5460; }
.badge-muted { background: #e9ecef; color: #495057; }
.reason-badge {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
/* Truncation + mono timestamp */
.cell-ellipsis {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: default;
}
.cell-mono {
font-family: 'SF Mono', 'Consolas', 'Monaco', monospace;
font-size: 12px;
color: #4a5568;
letter-spacing: -.02em;
}
.cell-copy-link { cursor: copy; }
.cell-copy-link:hover { color: var(--log-primary-dark); }
.cell-ip { font-family: 'SF Mono', 'Consolas', monospace; font-size: 12px; }
.text-muted { color: #adb5bd; }
.fp-hdata-cell {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: 4px 8px;
max-width: 100%;
min-height: 24px;
}
.fp-hdata-summary {
flex: 1 1 140px;
min-width: 0;
font-size: 11.5px;
color: #475569;
line-height: 1.35;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fp-hdata-actions {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
flex: 0 0 auto;
}
.fp-hdata-btn {
display: inline-block;
padding: 1px 8px;
font-size: 11px;
border-radius: 4px;
background: #e8f4fd;
color: #0c5460;
cursor: pointer;
border: 1px solid #bee5eb;
width: fit-content;
}
.fp-hdata-btn:hover { background: #d1ecf1; }
/* 指纹 / API 审计列:解除 layui 固定行高,避免「判定详情」等按钮被裁切 */
.log-table-wrap .layui-table-body td[data-field="fp_hdata_text"],
.log-table-wrap .layui-table-body td[data-field="api_by_request_text"],
.log-table-wrap .layui-table-body td[data-field="api_by_response_text"],
.log-table-wrap .layui-table-body td[data-field="api_risk_request_text"],
.log-table-wrap .layui-table-body td[data-field="api_risk_response_text"] {
height: auto !important;
vertical-align: top;
}
.log-table-wrap .layui-table-body td[data-field="fp_hdata_text"] > .layui-table-cell,
.log-table-wrap .layui-table-body td[data-field="api_by_request_text"] > .layui-table-cell,
.log-table-wrap .layui-table-body td[data-field="api_by_response_text"] > .layui-table-cell,
.log-table-wrap .layui-table-body td[data-field="api_risk_request_text"] > .layui-table-cell,
.log-table-wrap .layui-table-body td[data-field="api_risk_response_text"] > .layui-table-cell {
height: auto !important;
max-height: none !important;
white-space: normal !important;
overflow: visible !important;
line-height: 1.35;
padding-top: 7px !important;
padding-bottom: 7px !important;
}
.fp-risk-explain { font-size: 13px; line-height: 1.55; color: #2c3e50; }
.fp-risk-explain-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.fp-risk-tag { font-size: 12px; padding: 2px 10px; border-radius: 999px; background: #eef2ff; color: #3730a3; font-weight: 600; }
.fp-risk-kv { width: 100%; border-collapse: collapse; margin-bottom: 14px; font-size: 12.5px; }
.fp-risk-kv th { text-align: left; width: 88px; padding: 6px 8px; background: #f8fafc; border: 1px solid #e2e8f0; vertical-align: top; color: #64748b; }
.fp-risk-kv td { padding: 6px 8px; border: 1px solid #e2e8f0; word-break: break-word; }
.fp-risk-reason-raw { font-family: 'SF Mono', Consolas, monospace; font-size: 12px; }
.fp-risk-section-title { font-weight: 600; margin: 12px 0 8px; font-size: 13px; color: #334155; }
.fp-risk-factor-list { margin: 0; padding-left: 1.2rem; }
.fp-risk-factor-list > li { margin-bottom: 10px; }
.fp-factor-text { font-weight: 500; margin-bottom: 4px; }
.fp-factor-fields { margin: 0; padding-left: 1rem; list-style: disc; }
.fp-factor-fields mark.fp-field-key { background: #fef3c7; padding: 0 4px; border-radius: 3px; font-weight: 600; }
.fp-factor-fields code { font-size: 11.5px; word-break: break-all; }
.fp-factor-miss { font-size: 12px; color: #94a3b8; }
.fp-hdata-json {
max-height: 320px;
overflow: auto;
background: #0f172a;
color: #e2e8f0;
padding: 12px;
border-radius: 8px;
font-size: 11.5px;
line-height: 1.45;
margin: 0;
white-space: pre-wrap;
word-break: break-all;
}
.fp-hdata-json .fp-hl-line { background: rgba(250, 204, 21, .18); display: inline-block; width: 100%; }
.log-nav { display: flex; gap: 8px; flex-wrap: wrap; }
.log-nav a {
display: inline-block; padding: 6px 14px; border-radius: 6px; font-size: .85rem;
text-decoration: none; border: 1px solid var(--log-border); background: #fff; color: var(--log-text);
}
.log-nav a.active { background: var(--log-primary); color: #fff; border-color: var(--log-primary); }
.fp-risk-conclusions { margin: 0 0 14px; padding: 0; list-style: none; }
.fp-risk-conclusions li {
padding: 9px 12px; margin-bottom: 8px; border-radius: 8px; font-size: 12.5px; line-height: 1.55;
border-left: 3px solid #16baaa; background: #f0faf9;
}
.fp-risk-conclusions li.warn { background: #fff8e6; border-left-color: #e6a23c; }
.fp-risk-conclusions li.alert { background: #fef2f2; border-left-color: #e74c3c; }
.fp-risk-conclusions li.ok { background: #f0fdf4; border-left-color: #22c55e; }
.fp-risk-conclusions li .fp-conc-action {
display: block; margin-top: 4px; font-size: 12px; color: #64748b;
}
.fp-risk-conclusions li a { color: #0e8d81; text-decoration: none; }
.fp-risk-conclusions li a:hover { text-decoration: underline; }
.col-settings-list {
list-style: none;
margin: 0;
padding: 0;
max-height: 420px;
overflow-y: auto;
}
.col-settings-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
margin-bottom: 6px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
cursor: grab;
user-select: none;
}
.col-settings-item.dragging { opacity: .55; border-style: dashed; }
.col-settings-item.drag-over { border-color: var(--log-primary); background: #eefaf8; }
.col-settings-handle {
color: #94a3b8;
font-size: 14px;
line-height: 1;
cursor: grab;
}
.col-settings-title { flex: 1; font-size: 13px; color: #334155; }
.col-settings-actions {
display: flex;
gap: 8px;
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid #e8ecf1;
}
.col-settings-tip {
font-size: 12px;
color: #64748b;
margin: 0 0 10px;
line-height: 1.5;
}
@media (max-width: 768px) {
.log-page { padding: 16px 12px 32px; }
.log-count { margin-left: 0; width: 100%; }
}
</style>
</head>
<body>
<div class="log-page">
<div class="log-page-header">
<div>
<h1>访问判断日志</h1>
<p>访客判定记录 · 支持筛选、导出与分页浏览</p>
</div>
<div class="log-nav">
<a href="table.php" class="active">访问日志</a>
<a href="simulate_test.php">模拟测试</a>
<a href="log_analytics.php">数据分析</a>
<a href="dashboard.php">配置中心</a>
</div>
</div>
<div class="log-card">
<div class="log-card-body">
<form class="layui-form layui-row layui-col-space16 log-filter-grid">
<div class="layui-col-md3">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-username"></i>
</div>
<input type="text" name="domain" value="" placeholder="域名" class="layui-input" lay-affix="clear">
</div>
</div>
<div class="layui-col-md3">
<div class="layui-input-wrap">
<input type="text" name="spage" value="" placeholder="访问页面,可输入配置名称搜索" class="layui-input" lay-affix="clear">
</div>
</div>
<div class="layui-col-md3">
<div class="layui-input-wrap">
<select name="result">
<option value="">全部</option>
<option value="true">TRUE(通过访问)</option>
<option value="false">FALSE(禁止访问)</option>
<option value="wait">检测中</option>
</select>
</div>
</div>
<div class="layui-col-md3">
<div class="layui-input-wrap">
<input type="text" name="country" placeholder="国家" lay-affix="clear" class="layui-input">
</div>
</div>
<div class="layui-col-md3">
<div class="layui-input-wrap">
<input type="text" name="IP" placeholder="IP" lay-affix="clear" class="layui-input">
</div>
</div>
<div class="layui-col-md3">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-date"></i>
</div>
<input type="text" name="visit_date" readonly placeholder="访问日期" class="layui-input demo-table-search-date">
</div>
</div>
<div class="layui-btn-container layui-col-xs12">
<a class="layui-btn" lay-submit href="javascript:void(0);" id="export" target="_blank">导出</a>
<button class="layui-btn" lay-submit lay-filter="demo-table-search">搜索</button>
<button type="reset" class="layui-btn layui-btn-primary">清除条件</button>
</div>
</form>
</div>
<div class="log-actions">
<button class="layui-btn layui-btn-sm" id="btn-refresh" onclick="resetTable()" lay-event="footerDemoBtn1">刷新</button>
<a href="simulate_test.php" class="layui-btn layui-btn-sm layui-btn-normal">模拟测试</a>
<button class="layui-btn layui-btn-sm layui-btn-normal" id="btn-col-settings" type="button">列设置</button>
<button class="layui-btn layui-btn-sm btn-danger" id="btn-clear" lay-on="confirm" lay-event="footerDemoBtn2">清空数据库</button>
<span class="log-count">当前查询数量:<strong id="total_nums">—</strong></span>
</div>
</div>
<div class="log-card log-table-wrap">
<div id="demo-laypage-all"></div>
<table class="layui-hide" id="ID-table-demo-search" lay-filter="test"></table>
</div>
</div>
<script src="/dist/layui.js"></script>
<script src="/dist/jquery.min.js"></script>
<script>
layui.use(function(){
var table = layui.table;
var form = layui.form;
var laydate = layui.laydate;
var layer = layui.layer;
var LINK_CELL_MAX = 150;
var COLUMN_STORAGE_KEY = 'cloak_log_table_cols_v1';
var TABLE_ID = 'ID-table-demo-search';
var colConfig = [];
var dragField = null;
function escapeHtml(s) {
if (s == null || s === '') return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function escapeAttr(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;');
}
function cellEllipsis(value, maxLen) {
var text = value == null ? '' : String(value);
if (!text) return '<span class="text-muted">—</span>';
var esc = escapeHtml(text);
var display = text.length > maxLen ? escapeHtml(text.slice(0, maxLen)) + '…' : esc;
return '<span class="cell-ellipsis" title="' + esc + '">' + display + '</span>';
}
function cellLinkEllipsis(value, maxLen) {
maxLen = maxLen || LINK_CELL_MAX;
var text = value == null ? '' : String(value);
if (!text) return '<span class="text-muted">—</span>';
var esc = escapeHtml(text);
var display = text.length > maxLen ? escapeHtml(text.slice(0, maxLen)) + '…' : esc;
return '<span class="cell-ellipsis cell-copy-link" title="双击复制完整内容" data-full="' + escapeAttr(text) + '">' + display + '</span>';
}
function badgeResult(val) {
var raw = val == null ? '' : String(val);
var v = raw.toLowerCase();
if (v === 'true' || v === '1') {
return '<span class="cloak-badge badge-success">TRUE</span>';
}
if (v === 'false' || v === '0') {
return '<span class="cloak-badge badge-error">FALSE</span>';
}
if (v === 'wait' || raw.indexOf('检测') !== -1) {
return '<span class="cloak-badge badge-info">检测中</span>';
}
return '<span class="cloak-badge badge-muted">' + escapeHtml(raw) + '</span>';
}
function badgeReason(reason) {
if (reason == null || reason === '') {
return '<span class="text-muted">—</span>';
}
var text = String(reason);
var cls = 'badge-info';
if (text.indexOf('未确定流量来源') !== -1) {
cls = 'badge-warning';
} else if (/过滤|黑名单|无效|代理|VPN|禁止|欺骗|无来源/i.test(text)) {
cls = 'badge-error';
} else if (/白名单|允许|通过/i.test(text)) {
cls = 'badge-success';
} else if (/^\d+$/.test(text.trim()) || /风险|WebDriver|Canvas|分辨率|自动化|指纹/i.test(text)) {
cls = 'badge-warning';
}
var esc = escapeHtml(text);
var short = text.length > 26 ? escapeHtml(text.slice(0, 26)) + '…' : esc;
return '<span class="cloak-badge ' + cls + ' reason-badge" title="' + esc + '">' + short + '</span>';
}
function formatJsonPretty(str) {
try {
return JSON.stringify(JSON.parse(str), null, 2);
} catch (e) {
return str;
}
}
function cellApiAudit(val, title, d, field) {
if (val === '__LAZY__' && d && d.id && field) {
return '<div class="fp-hdata-cell">'
+ '<span class="fp-hdata-summary text-muted">已记录</span>'
+ '<span class="fp-hdata-actions"><span class="fp-hdata-btn fp-hdata-btn-json" data-log-id="' + escapeAttr(String(d.id)) + '" data-field="' + escapeAttr(field) + '" data-title="' + escapeAttr(title || 'API 数据') + '">查看 JSON</span></span>'
+ '</div>';
}
if (val == null || val === '' || val === '-') {
return '<span class="text-muted">—</span>';
}
var text = String(val);
var short = text.length > 40 ? text.slice(0, 40) + '…' : text;
return '<div class="fp-hdata-cell">'
+ '<span class="fp-hdata-summary" title="' + escapeAttr(text) + '">' + escapeHtml(short) + '</span>'
+ '<span class="fp-hdata-actions"><span class="fp-hdata-btn fp-hdata-btn-json" data-full="' + escapeAttr(text) + '" data-title="' + escapeAttr(title || 'API 数据') + '">查看 JSON</span></span>'
+ '</div>';
}
function cellFpHdata(d) {
if (!d.fp_hdata_text) {
return '<span class="text-muted">—</span>';
}
var summary = d.fp_hdata_text || '';
var html = '<div class="fp-hdata-cell">';
if (summary) {
html += '<span class="fp-hdata-summary" title="' + escapeAttr(summary) + '">' + escapeHtml(summary.length > 48 ? summary.slice(0, 48) + '…' : summary) + '</span>';
}
html += '<span class="fp-hdata-actions">';
html += '<span class="fp-hdata-btn" data-explain-id="' + escapeAttr(String(d.id)) + '">判定详情</span>';
html += '<span class="fp-hdata-btn fp-hdata-btn-raw" data-log-id="' + escapeAttr(String(d.id)) + '" data-field="fp_hdata">原始 hdata</span>';
html += '</span></div>';
return html;
}
var COLUMN_DEFS = [
{ field: 'id', title: 'ID', width: 56, fixed: 'left', defaultVisible: true },
{ field: 'domain', title: '域名', width: 120, defaultVisible: true, templet: function(d){ return cellEllipsis(d.domain, 18); } },
{ field: 'visit_date', title: '访问日期', width: 130, defaultVisible: true, templet: function(d){
return '<span class="cell-mono" title="' + escapeHtml(d.visit_date) + '">' + escapeHtml(d.visit_date) + '</span>';
}},
{ field: 'country', title: '国家', width: 100, defaultVisible: true, templet: function(d){ return cellEllipsis(d.country, 12); } },
{ field: 'result', title: '是否正常', width: 88, defaultVisible: true, templet: function(d){ return badgeResult(d.result); } },
{ field: 'reason', title: '理由', width: 140, defaultVisible: true, templet: function(d){ return badgeReason(d.reason); } },
{ field: 'judge_timing_text', title: '判断用时', width: 240, defaultVisible: true, templet: function(d){ return cellEllipsis(d.judge_timing_text, 36); } },
{ field: 'vtimes', title: '第几次访问', width: 96, defaultVisible: true },
{ field: 'fp_url', title: '跳转至', width: 280, defaultVisible: true, templet: function(d){ return cellLinkEllipsis(d.fp_url, LINK_CELL_MAX); } },
{ field: 'device', title: '访问设备', width: 200, defaultVisible: true, templet: function(d){ return cellEllipsis(d.device, 32); } },
{ field: 'referer', title: '来源', width: 280, defaultVisible: true, templet: function(d){ return cellLinkEllipsis(d.referer, LINK_CELL_MAX); } },
{ field: 'IP', title: 'IP(支持IPV6)', width: 130, defaultVisible: true, templet: function(d){
return '<span class="cell-ip" title="' + escapeHtml(d.IP) + '">' + escapeHtml(d.IP) + '</span>';
}},
{ field: 'page', title: '访问页面', width: 280, defaultVisible: true, templet: function(d){ return cellLinkEllipsis(d.page, LINK_CELL_MAX); } },
{ field: 'client', title: '客户端', width: 80, defaultVisible: true, templet: function(d){ return cellEllipsis(d.client, 10); } },
{ field: 'language', title: '语言', width: 80, defaultVisible: true, templet: function(d){ return cellEllipsis(d.language, 10); } },
{ field: 'browser', title: '浏览器', width: 88, defaultVisible: true, templet: function(d){ return cellEllipsis(d.browser, 10); } },
{ field: 'campagin_id', title: '广告策略', width: 88, defaultVisible: true, templet: function(d){ return cellEllipsis(d.campagin_id, 10); } },
{ field: 'user_agent', title: 'User-Agent', width: 320, defaultVisible: false, templet: function(d){ return cellEllipsis(d.user_agent, 48); } },
{ field: 'http_referer', title: 'HTTP-Referer', width: 260, defaultVisible: false, templet: function(d){ return cellLinkEllipsis(d.http_referer, LINK_CELL_MAX); } },
{ field: 'accept_language_raw', title: 'Accept-Language', width: 220, defaultVisible: false, templet: function(d){ return cellEllipsis(d.accept_language_raw, 32); } },
{ field: 'fp_hdata_text', title: '指纹 hdata', width: 360, defaultVisible: false, templet: function(d){ return cellFpHdata(d); } },
{ field: 'api_by_request_text', title: 'byApi 请求参数', width: 300, defaultVisible: false, templet: function(d){ return cellApiAudit(d.api_by_request_text, 'byApi 请求参数 #' + d.id, d, 'api_by_request'); } },
{ field: 'api_by_response_text', title: 'byApi 返回值', width: 300, defaultVisible: false, templet: function(d){ return cellApiAudit(d.api_by_response_text, 'byApi 返回值 #' + d.id, d, 'api_by_response'); } },
{ field: 'api_risk_request_text', title: 'byApiRisk 请求参数', width: 300, defaultVisible: false, templet: function(d){ return cellApiAudit(d.api_risk_request_text, 'byApiRisk 请求参数 #' + d.id, d, 'api_risk_request'); } },
{ field: 'api_risk_response_text', title: 'byApiRisk 返回值', width: 300, defaultVisible: false, templet: function(d){ return cellApiAudit(d.api_risk_response_text, 'byApiRisk 返回值 #' + d.id, d, 'api_risk_response'); } },
{ field: 'sim_tag', title: '模拟', width: 88, defaultVisible: true, templet: function(d){
if (String(d.is_simulation) === '1') {
var src = d.sim_source_log_id ? ' #' + d.sim_source_log_id : '';
return '<span class="cloak-badge badge-info">是' + escapeHtml(String(src)) + '</span>';
}
return '<span class="text-muted">—</span>';
}},
{ field: 'sim_action', title: '操作', width: 108, defaultVisible: true, templet: function(d){
return '<a class="layui-btn layui-btn-xs layui-btn-normal" href="simulate_test.php?from_log=' + encodeURIComponent(String(d.id)) + '">模拟测试</a>';
}},
];
var COLUMN_DEF_MAP = {};
COLUMN_DEFS.forEach(function(def) {
COLUMN_DEF_MAP[def.field] = def;
});
function getDefaultColConfig() {
return COLUMN_DEFS.map(function(def) {
return { field: def.field, visible: def.defaultVisible !== false };
});
}
function normalizeColConfig(raw) {
var defaults = getDefaultColConfig();
if (!Array.isArray(raw) || !raw.length) {
return defaults;
}
var seen = {};
var merged = [];
raw.forEach(function(item) {
if (!item || !item.field || !COLUMN_DEF_MAP[item.field] || seen[item.field]) {
return;
}
seen[item.field] = true;
merged.push({
field: item.field,
visible: item.visible !== false
});
});
defaults.forEach(function(item) {
if (!seen[item.field]) {
merged.push(item);
}
});
return merged;
}
function loadColConfig() {
try {
var raw = localStorage.getItem(COLUMN_STORAGE_KEY);
if (!raw) {
return getDefaultColConfig();
}
return normalizeColConfig(JSON.parse(raw));
} catch (e) {
return getDefaultColConfig();
}
}
function saveColConfig(config) {
colConfig = normalizeColConfig(config);
localStorage.setItem(COLUMN_STORAGE_KEY, JSON.stringify(colConfig));
}
function buildColItem(def, visible) {
var col = {
field: def.field,
title: def.title,
width: def.width,
hide: !visible
};
if (def.templet) {
col.templet = def.templet;
}
return col;
}
function buildCols(config) {
config = normalizeColConfig(config || colConfig);
var cols = [];
var firstVisibleField = null;
config.forEach(function(item) {
if (item.visible && !firstVisibleField) {
firstVisibleField = item.field;
}
});
config.forEach(function(item) {
var def = COLUMN_DEF_MAP[item.field];
if (!def) {
return;
}
var col = buildColItem(def, item.visible);
if (def.fixed && item.field === firstVisibleField) {
col.fixed = def.fixed;
} else {
delete col.fixed;
}
cols.push(col);
});
if (!cols.length) {
cols.push(buildColItem(COLUMN_DEF_MAP.id, true));
}
return [cols];
}
function fixRichTableCellHeights() {
var fields = ['fp_hdata_text', 'api_by_request_text', 'api_by_response_text', 'api_risk_request_text', 'api_risk_response_text'];
fields.forEach(function(field) {
$('.log-table-wrap .layui-table-body td[data-field="' + field + '"]').each(function() {
var $cell = $(this).children('.layui-table-cell');
$cell.css({ height: 'auto', whiteSpace: 'normal', overflow: 'visible' });
$(this).css('height', 'auto');
});
});
}
function applyTableRowStyles(res) {
if (!res || !Array.isArray(res.data)) {
return;
}
var $rows = $('.log-table-wrap .layui-table-body table tbody tr');
res.data.forEach(function(row, idx) {
var $tr = $rows.eq(idx);
if (!$tr.length) {
return;
}
if (row.reason && String(row.reason).indexOf('未确定流量来源') !== -1) {
$tr.css({ color: '#856404', backgroundColor: '#fff8e6' });
}
});
}
function onTableDataRendered(res, count) {
if (typeof count === 'number') {
$("#total_nums").text(count);
}
applyTableRowStyles(res);
fixRichTableCellHeights();
}
function reloadTableCols() {
table.reload(TABLE_ID, {
cols: buildCols(colConfig),
done: function(res, curr, count) {
onTableDataRendered(res, count);
}
});
}
function renderColSettingsList() {
var html = '<p class="col-settings-tip">勾选显示列,拖拽左侧手柄调整顺序。设置保存在本浏览器。</p>';
html += '<ul class="col-settings-list" id="col-settings-list">';
colConfig.forEach(function(item) {
var def = COLUMN_DEF_MAP[item.field];
if (!def) {
return;
}
html += '<li class="col-settings-item" draggable="true" data-field="' + escapeAttr(item.field) + '">';
html += '<span class="col-settings-handle" title="拖拽排序">☰</span>';
html += '<input type="checkbox" lay-skin="primary" class="col-settings-check" data-field="' + escapeAttr(item.field) + '"' + (item.visible ? ' checked' : '') + '>';
html += '<span class="col-settings-title">' + escapeHtml(def.title) + '</span>';
html += '</li>';
});
html += '</ul>';
html += '<div class="col-settings-actions">';
html += '<button type="button" class="layui-btn layui-btn-sm" id="col-settings-apply">应用</button>';
html += '<button type="button" class="layui-btn layui-btn-sm layui-btn-primary" id="col-settings-reset">恢复默认</button>';
html += '</div>';
return html;
}
function refreshColSettingsPanel($panel) {
var checks = {};
$panel.find('.col-settings-item').each(function() {
var field = this.getAttribute('data-field');
checks[field] = $(this).find('.col-settings-check').prop('checked');
});
colConfig.forEach(function(item) {
if (Object.prototype.hasOwnProperty.call(checks, item.field)) {
item.visible = !!checks[item.field];
}
});
$panel.find('.col-settings-body').html(renderColSettingsList());
form.render('checkbox');
}
function bindColSettingsEvents(layerIndex, $panel) {
$panel.off('.colSettings');
$panel.on('dragstart.colSettings', '.col-settings-item', function(e) {
dragField = this.getAttribute('data-field');
this.classList.add('dragging');
if (e.originalEvent && e.originalEvent.dataTransfer) {
e.originalEvent.dataTransfer.effectAllowed = 'move';
}
});
$panel.on('dragend.colSettings', '.col-settings-item', function() {
this.classList.remove('dragging');
$panel.find('.col-settings-item').removeClass('drag-over');
dragField = null;
});
$panel.on('dragover.colSettings', '.col-settings-item', function(e) {
e.preventDefault();
$panel.find('.col-settings-item').removeClass('drag-over');
this.classList.add('drag-over');
});
$panel.on('drop.colSettings', '.col-settings-item', function(e) {
e.preventDefault();
var targetField = this.getAttribute('data-field');
if (!dragField || !targetField || dragField === targetField) {
return;
}
var fromIdx = -1;
var toIdx = -1;
colConfig.forEach(function(item, idx) {
if (item.field === dragField) fromIdx = idx;
if (item.field === targetField) toIdx = idx;
});
if (fromIdx < 0 || toIdx < 0) {
return;
}
var moved = colConfig.splice(fromIdx, 1)[0];
colConfig.splice(toIdx, 0, moved);
refreshColSettingsPanel($panel);
});
$panel.on('click.colSettings', '#col-settings-apply', function() {
var next = [];
$panel.find('.col-settings-item').each(function() {
var field = this.getAttribute('data-field');
var visible = $(this).find('.col-settings-check').prop('checked');
next.push({ field: field, visible: !!visible });
});
if (!next.some(function(item) { return item.visible; })) {
layer.msg('至少保留一列显示');
return;
}
saveColConfig(next);
reloadTableCols();
layer.close(layerIndex);
layer.msg('列设置已应用');
});
$panel.on('click.colSettings', '#col-settings-reset', function() {
colConfig = getDefaultColConfig();
refreshColSettingsPanel($panel);
});
}
function openColSettings() {
colConfig = loadColConfig();
var shell = '<div class="col-settings-body">' + renderColSettingsList() + '</div>';
var layerIndex = layer.open({
type: 1,
title: '表格列设置',
area: ['360px', '560px'],
shadeClose: true,
content: '<div class="col-settings-panel" style="padding:14px 16px 16px;">' + shell + '</div>',
success: function(layero) {
var $panel = layero.find('.col-settings-panel');
form.render('checkbox');
bindColSettingsEvents(layerIndex, $panel);
}
});
}
colConfig = loadColConfig();
table.render({
elem: '#ID-table-demo-search',
id: TABLE_ID,
url: 'datatable/datalist.php',
cols: buildCols(colConfig),
page: true,
limit: 50,
height: 800,
size: "sm",
skin: "line",
even: true,
done: function(res, curr, count, origin){
onTableDataRendered(res, count);
},
});
$('#btn-col-settings').on('click', openColSettings);
$(document).on('click', '.fp-hdata-btn', function(e) {
e.stopPropagation();
var id = this.getAttribute('data-explain-id');
var logId = this.getAttribute('data-log-id');
var field = this.getAttribute('data-field');
var full = this.getAttribute('data-full') || '';
if (id) {
if (window.__fpExplainCache && window.__fpExplainCache[id]) {
layer.open({
type: 1,
title: '二次风控判定解析 #' + id,
area: ['720px', '85vh'],
shadeClose: true,
content: '<div style="padding:16px 18px;max-height:calc(85vh - 60px);overflow:auto;">' + window.__fpExplainCache[id] + '</div>'
});
return;
}
var loadIdx = layer.load(1);
$.getJSON('datatable/fp_explain.php', { id: id }, function(res) {
layer.close(loadIdx);
if (!res || res.code !== 0 || !res.html) {
layer.msg((res && res.msg) ? res.msg : '加载判定详情失败');
return;
}
window.__fpExplainCache = window.__fpExplainCache || {};
window.__fpExplainCache[id] = res.html;
layer.open({
type: 1,
title: '二次风控判定解析 #' + id,
area: ['720px', '85vh'],
shadeClose: true,
content: '<div style="padding:16px 18px;max-height:calc(85vh - 60px);overflow:auto;">' + res.html + '</div>'
});
}).fail(function() {
layer.close(loadIdx);
layer.msg('加载判定详情失败');
});
return;
}
if (logId && field) {
var popupTitle = this.getAttribute('data-title') || (field === 'fp_hdata' ? '指纹 hdata' : field);
var isJson = this.classList.contains('fp-hdata-btn-json') || field.indexOf('api_') === 0 || field === 'fp_hdata';
var loadIdx2 = layer.load(1);
$.getJSON('datatable/log_blob.php', { id: logId, field: field }, function(res) {
layer.close(loadIdx2);
if (!res || res.code !== 0 || !res.data) {
layer.msg((res && res.msg) ? res.msg : '加载失败');
return;
}
var body = isJson ? escapeHtml(formatJsonPretty(res.data)) : escapeHtml(res.data);
layer.open({
type: 1,
title: popupTitle + ' #' + logId,
area: ['680px', '70vh'],
shadeClose: true,
content: '<pre style="padding:14px;margin:0;max-height:calc(70vh - 50px);overflow:auto;font-size:12px;white-space:pre-wrap;word-break:break-all;">' + body + '</pre>'
});
}).fail(function() {
layer.close(loadIdx2);
layer.msg('加载失败');
});
return;
}
if (full) {
var popupTitleStatic = this.getAttribute('data-title') || '指纹 hdata';
var bodyStatic = this.classList.contains('fp-hdata-btn-json')
? escapeHtml(formatJsonPretty(full))
: escapeHtml(full);
layer.open({
type: 1,
title: popupTitleStatic,
area: ['680px', '70vh'],
shadeClose: true,
content: '<pre style="padding:14px;margin:0;max-height:calc(70vh - 50px);overflow:auto;font-size:12px;white-space:pre-wrap;word-break:break-all;">' + bodyStatic + '</pre>'
});
}
});
$(document).on('dblclick', '.cell-copy-link', function() {
var full = this.getAttribute('data-full') || '';
if (!full) return;
var notify = function() { layer.msg('已复制完整内容'); };
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(full).then(notify).catch(function() {
layer.msg('复制失败');
});
return;
}
var ta = document.createElement('textarea');
ta.value = full;
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
notify();
} catch (e) {
layer.msg('复制失败');
}
document.body.removeChild(ta);
});
// 日期
laydate.render({
elem: '.demo-table-search-date'
});
// 搜索提交
form.on('submit(demo-table-search)', function(data){
var field = data.field; // 获得表单字段
// 执行搜索重载
table.reload('ID-table-demo-search', {
page: {
curr: 1 // 重新从第 1 页开始
},
where: field // 搜索的字段
});
// layer.msg('搜索成功<br>此处为静态模拟数据,实际使用时换成真实接口即可');
return false; // 阻止默认 form 跳转
});
// table.reload('ID-table-demo-search', {
// page: {
// curr: 1 // 重新从第 1 页开始
// },
// });
var util = layui.util;
util.on('lay-on', {
confirm: function(){
layer.confirm('确定清除所有日志数据?', {
btn: ['确定', '关闭'] //按钮
}, function(){
$.get("datatable/datalist.php", {clear: "true"}, function(result){
layer.msg('清除成功', {icon: 1});
window.location.reload();
});
}, function(){
});
},
});
});
// 刷新按钮
function resetTable() {
var table = layui.table;
table.reload('ID-table-demo-search', {
page: {
curr: 1 // 重新从第 1 页开始
},
});
}
$("#export").click(function() {
var visit_date = $("input[name=visit_date]").val();
var domain = $("input[name=domain]").val();
var spage = $("input[name=spage]").val();
var country = $("input[name=country]").val();
var IP = $("input[name=IP]").val();
var result = $("select[name=result]").val();
var url = 'datatable/export.php?';
var args = [];
if(visit_date) {
args.push( "visit_date=" + visit_date );
}
if(domain) {
args.push( "domain=" + domain );
}
if(spage) {
args.push( "spage=" + spage );
}
if(country) {
args.push( "country=" + country );
}
if(IP) {
args.push( "IP=" + IP );
}
if(result) {
args.push( "result=" + encodeURIComponent(result) );
}
window.location.href=url + args.join("&");
});
</script>
</body>
</html>