399 lines
16 KiB
Markdown
399 lines
16 KiB
Markdown
## 重写DNS日志详情弹窗方案
|
|
|
|
### 1. 问题分析
|
|
当前弹窗代码存在以下问题:
|
|
- 代码结构复杂,大量嵌套条件和重复逻辑
|
|
- 样式不够现代,布局不够灵活
|
|
- 响应式设计不足,在大屏幕上显示效果不佳
|
|
- 代码冗余,重复的记录解析逻辑
|
|
- 可维护性差,HTML结构直接写在JavaScript字符串中
|
|
|
|
### 2. 设计思路
|
|
- **组件化设计**:将弹窗拆分为更小的可复用组件
|
|
- **简化逻辑**:重构DNS解析记录处理逻辑,减少重复代码
|
|
- **现代样式**:使用更现代的CSS布局和样式
|
|
- **响应式设计**:优化在不同屏幕尺寸下的显示效果
|
|
- **更好的用户体验**:添加动画效果、改进交互体验
|
|
- **更清晰的代码结构**:提高可维护性
|
|
|
|
### 3. 实现方案
|
|
|
|
#### 3.1 重构`showLogDetailModal`函数
|
|
- 简化函数结构,分离关注点
|
|
- 将DNS解析记录处理逻辑提取为独立函数
|
|
- 使用更现代的DOM API创建元素
|
|
|
|
#### 3.2 优化DNS解析记录处理
|
|
- 创建独立的`formatDNSRecords`函数
|
|
- 统一处理各种格式的DNS记录
|
|
- 减少重复代码,提高可维护性
|
|
|
|
#### 3.3 现代CSS样式
|
|
- 使用更现代的CSS类名和布局
|
|
- 优化响应式设计,支持不同屏幕尺寸
|
|
- 添加动画效果,提升用户体验
|
|
|
|
#### 3.4 改进HTML结构
|
|
- 使用更清晰的语义化HTML
|
|
- 优化布局结构,提高可读性
|
|
- 支持更好的响应式设计
|
|
|
|
### 4. 代码实现
|
|
```javascript
|
|
// 独立的DNS记录格式化函数
|
|
function formatDNSRecords(log, result) {
|
|
if (result === 'blocked') return '无';
|
|
|
|
let records = '';
|
|
const sources = [
|
|
log.answers,
|
|
log.answer,
|
|
log.Records,
|
|
log.records,
|
|
log.response
|
|
];
|
|
|
|
for (const source of sources) {
|
|
if (records) break;
|
|
if (!source || source === '无') continue;
|
|
|
|
// 处理数组类型
|
|
if (Array.isArray(source)) {
|
|
records = source.map(answer => {
|
|
const type = answer.type || answer.Type || '未知';
|
|
let value = answer.value || answer.Value || answer.data || answer.Data || '未知';
|
|
const ttl = answer.TTL || answer.ttl || answer.expires || '未知';
|
|
|
|
// 增强的记录值提取逻辑
|
|
if (typeof value === 'string') {
|
|
value = value.trim();
|
|
// 处理制表符分隔的格式
|
|
if (value.includes('\t') || value.includes('\\t')) {
|
|
const parts = value.replace(/\\t/g, '\t').split('\t');
|
|
if (parts.length >= 4) {
|
|
value = parts[parts.length - 1].trim();
|
|
}
|
|
}
|
|
// 处理JSON格式
|
|
else if (value.startsWith('{') && value.endsWith('}')) {
|
|
try {
|
|
const parsed = JSON.parse(value);
|
|
value = parsed.data || parsed.value || value;
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
|
|
return `${type}: ${value} (ttl=${ttl})`;
|
|
}).join('\n').trim();
|
|
}
|
|
// 处理字符串类型
|
|
else if (typeof source === 'string') {
|
|
// 尝试解析为JSON数组
|
|
if (source.startsWith('[') && source.endsWith(']')) {
|
|
try {
|
|
const parsed = JSON.parse(source);
|
|
if (Array.isArray(parsed)) {
|
|
records = parsed.map(answer => {
|
|
const type = answer.type || answer.Type || '未知';
|
|
let value = answer.value || answer.Value || answer.data || answer.Data || '未知';
|
|
const ttl = answer.TTL || answer.ttl || answer.expires || '未知';
|
|
|
|
if (typeof value === 'string') {
|
|
value = value.trim();
|
|
}
|
|
|
|
return `${type}: ${value} (ttl=${ttl})`;
|
|
}).join('\n').trim();
|
|
}
|
|
} catch (e) {
|
|
// 解析失败,尝试直接格式化
|
|
records = formatDNSString(source);
|
|
}
|
|
} else {
|
|
// 直接格式化字符串
|
|
records = formatDNSString(source);
|
|
}
|
|
}
|
|
}
|
|
|
|
return records || '无解析记录';
|
|
}
|
|
|
|
// 格式化DNS字符串记录
|
|
function formatDNSString(str) {
|
|
const recordLines = str.split('\n').map(line => line.trim()).filter(line => line !== '');
|
|
|
|
return recordLines.map(line => {
|
|
// 检查是否已经是标准格式
|
|
if (line.includes(':') && line.includes('(')) {
|
|
return line;
|
|
}
|
|
// 尝试解析为标准DNS格式
|
|
const parts = line.split(/\s+/);
|
|
if (parts.length >= 5) {
|
|
const type = parts[3];
|
|
const value = parts.slice(4).join(' ');
|
|
const ttl = parts[1];
|
|
return `${type}: ${value} (ttl=${ttl})`;
|
|
}
|
|
// 无法解析,返回原始行
|
|
return line;
|
|
}).join('\n');
|
|
}
|
|
|
|
// 重写后的showLogDetailModal函数
|
|
async function showLogDetailModal(log) {
|
|
if (!log) {
|
|
console.error('No log data provided!');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 安全获取log属性,提供默认值
|
|
const timestamp = log.timestamp ? new Date(log.timestamp) : null;
|
|
const dateStr = timestamp ? timestamp.toLocaleDateString() : '未知';
|
|
const timeStr = timestamp ? timestamp.toLocaleTimeString() : '未知';
|
|
const domain = log.domain || '未知';
|
|
const queryType = log.queryType || '未知';
|
|
const result = log.result || '未知';
|
|
const responseTime = log.responseTime || '未知';
|
|
const clientIP = log.clientIP || '未知';
|
|
const location = log.location || '未知';
|
|
const fromCache = log.fromCache || false;
|
|
const dnssec = log.dnssec || false;
|
|
const edns = log.edns || false;
|
|
const dnsServer = log.dnsServer || '无';
|
|
const dnssecServer = log.dnssecServer || '无';
|
|
const blockRule = log.blockRule || '无';
|
|
|
|
// 检查域名是否在跟踪器数据库中
|
|
const trackerInfo = await isDomainInTrackerDatabase(log.domain);
|
|
const isTracker = trackerInfo !== null;
|
|
|
|
// 格式化DNS解析记录
|
|
const dnsRecords = formatDNSRecords(log, result);
|
|
|
|
// 创建模态框容器
|
|
const modalContainer = document.createElement('div');
|
|
modalContainer.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4 animate-fade-in';
|
|
modalContainer.style.zIndex = '9999';
|
|
|
|
// 创建模态框内容
|
|
const modalContent = document.createElement('div');
|
|
modalContent.className = 'bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto animate-slide-in';
|
|
|
|
// 创建标题栏
|
|
const header = document.createElement('div');
|
|
header.className = 'sticky top-0 bg-white border-b border-gray-200 px-6 py-4 flex justify-between items-center';
|
|
|
|
const title = document.createElement('h3');
|
|
title.className = 'text-xl font-semibold text-gray-900';
|
|
title.textContent = '日志详情';
|
|
|
|
const closeButton = document.createElement('button');
|
|
closeButton.innerHTML = '<i class="fa fa-times text-xl"></i>';
|
|
closeButton.className = 'text-gray-500 hover:text-gray-700 focus:outline-none transition-colors';
|
|
closeButton.onclick = () => closeModal();
|
|
|
|
header.appendChild(title);
|
|
header.appendChild(closeButton);
|
|
|
|
// 创建内容区域
|
|
const content = document.createElement('div');
|
|
content.className = 'p-6 space-y-6';
|
|
|
|
// 基本信息部分
|
|
const basicInfo = document.createElement('div');
|
|
basicInfo.className = 'space-y-4';
|
|
|
|
const basicInfoTitle = document.createElement('h4');
|
|
basicInfoTitle.className = 'text-sm font-medium text-gray-700 uppercase tracking-wider';
|
|
basicInfoTitle.textContent = '基本信息';
|
|
|
|
const basicInfoGrid = document.createElement('div');
|
|
basicInfoGrid.className = 'grid grid-cols-1 md:grid-cols-2 gap-4';
|
|
|
|
// 添加基本信息项
|
|
basicInfoGrid.innerHTML = `
|
|
<div class="space-y-1">
|
|
<div class="text-xs text-gray-500">日期</div>
|
|
<div class="text-sm font-medium text-gray-900">${dateStr}</div>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<div class="text-xs text-gray-500">时间</div>
|
|
<div class="text-sm font-medium text-gray-900">${timeStr}</div>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<div class="text-xs text-gray-500">状态</div>
|
|
<div class="text-sm font-medium ${result === 'blocked' ? 'text-red-600' : result === 'allowed' ? 'text-green-600' : 'text-gray-500'}">
|
|
${result === 'blocked' ? '已拦截' : result === 'allowed' ? '允许' : result}
|
|
</div>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<div class="text-xs text-gray-500">域名</div>
|
|
<div class="text-sm font-medium text-gray-900 break-all">${domain}</div>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<div class="text-xs text-gray-500">类型</div>
|
|
<div class="text-sm font-medium text-gray-900">${queryType}</div>
|
|
</div>
|
|
`;
|
|
|
|
// DNS特性
|
|
const dnsFeatures = document.createElement('div');
|
|
dnsFeatures.className = 'col-span-1 md:col-span-2 space-y-1';
|
|
dnsFeatures.innerHTML = `
|
|
<div class="text-xs text-gray-500">DNS特性</div>
|
|
<div class="text-sm font-medium text-gray-900">
|
|
${dnssec ? '<i class="fa fa-lock text-green-500 mr-1" title="DNSSEC已启用"></i>DNSSEC ' : ''}
|
|
${edns ? '<i class="fa fa-exchange text-blue-500 mr-1" title="EDNS已启用"></i>EDNS' : ''}
|
|
${!dnssec && !edns ? '无' : ''}
|
|
</div>
|
|
`;
|
|
|
|
// 跟踪器信息
|
|
const trackerDiv = document.createElement('div');
|
|
trackerDiv.className = 'col-span-1 md:col-span-2 space-y-1';
|
|
trackerDiv.innerHTML = `
|
|
<div class="text-xs text-gray-500">跟踪器信息</div>
|
|
<div class="text-sm font-medium text-gray-900">
|
|
${isTracker ? `
|
|
<div class="flex items-center">
|
|
<i class="fa fa-eye text-red-500 mr-1"></i>
|
|
<span>${trackerInfo.name} (${trackersDatabase.categories[trackerInfo.categoryId] || '未知'})</span>
|
|
</div>
|
|
` : '无'}
|
|
</div>
|
|
`;
|
|
|
|
// 解析记录
|
|
const recordsDiv = document.createElement('div');
|
|
recordsDiv.className = 'col-span-1 md:col-span-2 space-y-1';
|
|
recordsDiv.innerHTML = `
|
|
<div class="text-xs text-gray-500">解析记录</div>
|
|
<div class="text-sm font-medium text-gray-900 whitespace-pre-wrap break-all bg-gray-50 p-3 rounded-md border border-gray-200">
|
|
${dnsRecords}
|
|
</div>
|
|
`;
|
|
|
|
// DNS服务器
|
|
const dnsServerDiv = document.createElement('div');
|
|
dnsServerDiv.className = 'col-span-1 md:col-span-2 space-y-1';
|
|
dnsServerDiv.innerHTML = `
|
|
<div class="text-xs text-gray-500">DNS服务器</div>
|
|
<div class="text-sm font-medium text-gray-900">${dnsServer}</div>
|
|
`;
|
|
|
|
// DNSSEC专用服务器
|
|
const dnssecServerDiv = document.createElement('div');
|
|
dnssecServerDiv.className = 'col-span-1 md:col-span-2 space-y-1';
|
|
dnssecServerDiv.innerHTML = `
|
|
<div class="text-xs text-gray-500">DNSSEC专用服务器</div>
|
|
<div class="text-sm font-medium text-gray-900">${dnssecServer}</div>
|
|
`;
|
|
|
|
basicInfoGrid.appendChild(dnsFeatures);
|
|
basicInfoGrid.appendChild(trackerDiv);
|
|
basicInfoGrid.appendChild(recordsDiv);
|
|
basicInfoGrid.appendChild(dnsServerDiv);
|
|
basicInfoGrid.appendChild(dnssecServerDiv);
|
|
|
|
basicInfo.appendChild(basicInfoTitle);
|
|
basicInfo.appendChild(basicInfoGrid);
|
|
|
|
// 响应细节部分
|
|
const responseDetails = document.createElement('div');
|
|
responseDetails.className = 'space-y-4 pt-4 border-t border-gray-200';
|
|
|
|
const responseDetailsTitle = document.createElement('h4');
|
|
responseDetailsTitle.className = 'text-sm font-medium text-gray-700 uppercase tracking-wider';
|
|
responseDetailsTitle.textContent = '响应细节';
|
|
|
|
const responseGrid = document.createElement('div');
|
|
responseGrid.className = 'grid grid-cols-1 md:grid-cols-2 gap-4';
|
|
responseGrid.innerHTML = `
|
|
<div class="space-y-1">
|
|
<div class="text-xs text-gray-500">响应时间</div>
|
|
<div class="text-sm font-medium text-gray-900">${responseTime}毫秒</div>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<div class="text-xs text-gray-500">规则</div>
|
|
<div class="text-sm font-medium text-gray-900">${blockRule}</div>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<div class="text-xs text-gray-500">响应代码</div>
|
|
<div class="text-sm font-medium text-gray-900">${getResponseCodeText(log.responseCode)}</div>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<div class="text-xs text-gray-500">缓存状态</div>
|
|
<div class="text-sm font-medium ${fromCache ? 'text-primary' : 'text-gray-500'}">
|
|
${fromCache ? '缓存' : '非缓存'}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
responseDetails.appendChild(responseDetailsTitle);
|
|
responseDetails.appendChild(responseGrid);
|
|
|
|
// 客户端详情部分
|
|
const clientDetails = document.createElement('div');
|
|
clientDetails.className = 'space-y-4 pt-4 border-t border-gray-200';
|
|
|
|
const clientDetailsTitle = document.createElement('h4');
|
|
clientDetailsTitle.className = 'text-sm font-medium text-gray-700 uppercase tracking-wider';
|
|
clientDetailsTitle.textContent = '客户端详情';
|
|
|
|
const clientIPDiv = document.createElement('div');
|
|
clientIPDiv.className = 'space-y-1';
|
|
clientIPDiv.innerHTML = `
|
|
<div class="text-xs text-gray-500">IP地址</div>
|
|
<div class="text-sm font-medium text-gray-900">${clientIP} (${location})</div>
|
|
`;
|
|
|
|
clientDetails.appendChild(clientDetailsTitle);
|
|
clientDetails.appendChild(clientIPDiv);
|
|
|
|
// 组装内容
|
|
content.appendChild(basicInfo);
|
|
content.appendChild(responseDetails);
|
|
content.appendChild(clientDetails);
|
|
|
|
// 组装模态框
|
|
modalContent.appendChild(header);
|
|
modalContent.appendChild(content);
|
|
modalContainer.appendChild(modalContent);
|
|
|
|
// 添加到页面
|
|
document.body.appendChild(modalContainer);
|
|
|
|
// 关闭模态框函数
|
|
function closeModal() {
|
|
modalContainer.classList.add('animate-fade-out');
|
|
modalContent.classList.add('animate-slide-out');
|
|
|
|
// 等待动画结束后移除元素
|
|
setTimeout(() => {
|
|
document.body.removeChild(modalContainer);
|
|
}, 300);
|
|
}
|
|
|
|
// 点击外部关闭
|
|
modalContainer.addEventListener('click', (e) => {
|
|
if (e.target === modalContainer) {
|
|
closeModal();
|
|
}
|
|
});
|
|
|
|
// ESC键关闭
|
|
const handleEsc = (e) => {
|
|
if (e.key === 'Escape') {
|
|
closeModal();
|
|
document.removeEventListener('keydown', handleEsc);
|
|
}
|
|
};
|
|
document.addEventListener('keydown', handleEsc);
|
|
|
|
} catch (error) {
|
|
console.error('Error showing log detail modal:', error);
|
|
}
|
|
} |