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);
|
||
}
|
||
} |