XNSim/XNSimPortal/components/network-monitor.js

611 lines
23 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @class NetworkMonitor
* @description 网络监控组件用于监控系统网络状态和UDP数据抓包
*/
class NetworkMonitor extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.state = {
ip: '127.0.0.1',
port: 54321,
isMonitoring: false,
data: [],
timer: null,
statusMsg: '',
selectedData: null,
};
}
connectedCallback() {
this.render();
this.initialize();
}
disconnectedCallback() {
if (this.state.timer) {
clearInterval(this.state.timer);
}
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
font-family: 'Segoe UI', 'PingFang SC', Arial, sans-serif;
background: #f4f6fa;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.07);
padding: 24px 20px 20px 20px;
width: 100%;
min-width: 400px;
max-width: 100%;
height: 100%;
overflow-y: auto;
box-sizing: border-box;
}
.container {
display: flex;
height: 100%;
gap: 20px;
}
.left-panel {
flex: 1;
min-width: 0;
}
.right-panel {
flex: 1;
min-width: 0;
}
.form-row {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 18px;
}
label {
font-size: 15px;
color: #333;
}
input[type="text"], input[type="number"] {
border: 1px solid #d0d7de;
border-radius: 6px;
padding: 6px 12px;
font-size: 15px;
outline: none;
transition: border 0.2s;
background: #fff;
margin-left: 4px;
}
input[type="text"]:focus, input[type="number"]:focus {
border: 1.5px solid #409eff;
}
.status {
display: flex;
align-items: center;
background: #eaf6ff;
color: #1976d2;
border-radius: 8px;
padding: 8px 14px;
margin-bottom: 16px;
font-size: 15px;
font-weight: 500;
box-shadow: 0 1px 4px rgba(64,158,255,0.07);
}
.status::before {
content: '\u26A1';
margin-right: 8px;
font-size: 18px;
}
button {
border: none;
border-radius: 6px;
padding: 7px 20px;
font-size: 15px;
cursor: pointer;
transition: background 0.2s, box-shadow 0.2s;
box-shadow: 0 1px 4px rgba(64,158,255,0.07);
}
#startBtn {
background: linear-gradient(90deg, #409eff 60%, #66b1ff 100%);
color: #fff;
font-weight: 600;
}
#startBtn:hover {
background: linear-gradient(90deg, #1976d2 60%, #409eff 100%);
}
#stopBtn {
background: #fff0f0;
color: #e53935;
font-weight: 600;
border: 1px solid #ffcdd2;
}
#stopBtn:hover {
background: #ffcdd2;
}
.monitor-status {
margin-bottom: 14px;
font-size: 15px;
color: #666;
}
.monitor-status span {
font-weight: bold;
color: #43a047;
}
.monitor-status span.stopped {
color: #e53935;
}
.data-list {
height: calc(100% - 143px);
min-height: 200px;
overflow-y: auto;
background: #fff;
border-radius: 10px;
border: 1px solid #e0e3e7;
padding: 10px 0 10px 0;
box-shadow: 0 1px 6px rgba(0,0,0,0.04);
width: 100%;
box-sizing: border-box;
}
.data-item {
background: #f8fafc;
border-radius: 8px;
margin: 10px 16px;
padding: 10px 14px 8px 14px;
box-shadow: 0 1px 3px rgba(64,158,255,0.04);
border-left: 4px solid #409eff;
font-size: 14px;
transition: box-shadow 0.2s;
cursor: pointer;
}
.data-item:hover {
box-shadow: 0 2px 8px rgba(64,158,255,0.13);
}
.data-item.selected {
background: #e3f2fd;
border-left-color: #1976d2;
box-shadow: 0 2px 8px rgba(25,118,210,0.2);
}
.data-item b {
color: #1976d2;
}
.detail-panel {
background: #fff;
border-radius: 10px;
border: 1px solid #e0e3e7;
padding: 20px;
box-shadow: 0 1px 6px rgba(0,0,0,0.04);
height: calc(100% - 40px);
overflow-y: auto;
}
.detail-header {
margin-bottom: 6px;
padding-bottom: 4px;
border-bottom: 2px solid #e3f2fd;
}
.detail-header h2 {
margin: 0 0 10px 0;
color: #1976d2;
font-size: 20px;
font-weight: 600;
}
.hex-content {
background: #f8fafc;
border-radius: 8px;
padding: 16px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.4;
white-space: pre-wrap;
word-break: break-all;
border: 1px solid #e0e3e7;
height: 250px;
max-height: 250px;
overflow-y: auto;
overflow-x: hidden;
box-sizing: border-box;
}
.parse-content {
background: #f8fafc;
border-radius: 8px;
padding: 16px;
font-family: 'Segoe UI', sans-serif;
font-size: 14px;
line-height: 1.6;
border: 1px solid #e0e3e7;
margin-top: 16px;
max-height: 400px;
overflow-y: auto;
}
.parse-item {
margin-bottom: 12px;
padding: 8px 12px;
background: #fff;
border-radius: 6px;
border-left: 4px solid #409eff;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.parse-item b {
color: #1976d2;
margin-right: 8px;
font-size: 16px;
}
.parse-item .hex-value {
background: #e3f2fd;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 12px;
color: #1976d2;
margin: 0 4px;
}
.hex-content .offset {
color: #666;
font-weight: bold;
}
.hex-content .hex {
color: #1976d2;
}
.no-selection {
text-align: center;
padding: 60px 20px;
color: #999;
font-style: italic;
font-size: 16px;
}
.no-selection::before {
content: '📋';
display: block;
font-size: 48px;
margin-bottom: 16px;
}
.nodata {
color: #bbb;
text-align: center;
padding: 30px 0;
}
.detail-title {
font-size: 16px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 4px;
padding: 8px 0;
border-bottom: 2px solid #409eff;
}
</style>
<div class="container">
<div class="left-panel">
<div class="status">${this.state.statusMsg}</div>
<div class="form-row">
<label>IP: <input type="text" id="ip" value="${this.state.ip}" /></label>
<label>端口: <input type="number" id="port" value="${this.state.port}" min="1024" max="65535" /></label>
<button id="startBtn">开始抓包</button>
<button id="stopBtn">停止抓包</button>
</div>
<div class="monitor-status">抓包状态:<span id="monitorStatus" class="${this.state.isMonitoring ? '' : 'stopped'}">${this.state.isMonitoring ? '运行中' : '已停止'}</span></div>
<div class="data-list" id="dataList"></div>
</div>
<div class="right-panel">
<div class="detail-panel">
<div class="detail-header">
<h2>UDP数据包详情</h2>
</div>
<div id="detailContent">
<div class="no-selection">请从左侧选择一个数据包查看详情</div>
</div>
</div>
</div>
</div>
`;
this.shadowRoot.getElementById('startBtn').onclick = () => this.startMonitor();
this.shadowRoot.getElementById('stopBtn').onclick = () => this.stopMonitor();
this.shadowRoot.getElementById('ip').onchange = (e) => {
this.state.ip = e.target.value;
};
this.shadowRoot.getElementById('port').onchange = (e) => {
this.state.port = parseInt(e.target.value, 10);
};
this.renderDataList();
}
renderDataList() {
const dataList = this.shadowRoot.getElementById('dataList');
if (!dataList) return;
if (!this.state.data.length) {
dataList.innerHTML = '<div class="nodata">暂无数据</div>';
return;
}
dataList.innerHTML = this.state.data.map((item, index) => `
<div class="data-item ${this.state.selectedData === index ? 'selected' : ''}" data-index="${index}">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="width: 40%; padding: 2px 0;"><b>时间:</b> ${new Date(item.timestamp).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3
})}</td>
<td style="width: 35%; padding: 2px 0;"><b>来源:</b> ${item.source}</td>
<td style="width: 25%; padding: 2px 0;"><b>大小:</b> ${item.length || this.getDataSize(item.data)} 字节</td>
</tr>
</table>
</div>
`).join('');
// 添加点击事件
dataList.querySelectorAll('.data-item').forEach(item => {
item.addEventListener('click', (e) => {
const index = parseInt(e.currentTarget.dataset.index);
this.selectDataItem(index);
});
});
}
getDataSize(data) {
// 如果数据是十六进制字符串,每两个字符代表一个字节
if (typeof data === 'string' && /^[0-9a-fA-F]+$/.test(data)) {
return Math.ceil(data.length / 2);
}
return 0;
}
formatDataAsHex(data) {
// 如果数据是十六进制字符串,直接处理
if (typeof data === 'string' && /^[0-9a-fA-F]+$/.test(data)) {
const bytes = [];
for (let i = 0; i < data.length; i += 2) {
const byte = parseInt(data.substr(i, 2), 16);
bytes.push(byte);
}
// 分行显示每行16个字节
const bytesPerLine = 16;
const lines = [];
for (let i = 0; i < bytes.length; i += bytesPerLine) {
const lineBytes = bytes.slice(i, i + bytesPerLine);
const lineHex = lineBytes.map(byte => byte.toString(16).padStart(2, '0')).join(' ');
const offset = i.toString(16).padStart(4, '0').toUpperCase();
lines.push(`${offset}: ${lineHex}`);
}
return lines.join('\n');
}
return '无效数据格式';
}
setStatus(msg) {
this.state.statusMsg = msg;
this.render();
}
async startMonitor() {
this.setStatus('正在启动UDP抓包...');
try {
const res = await fetch('/api/udp-monitor/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ip: this.state.ip, port: this.state.port })
});
const data = await res.json();
if (data.success) {
this.state.isMonitoring = true;
this.setStatus(data.message || 'UDP抓包已启动');
this.startPolling();
} else {
this.state.isMonitoring = false;
this.setStatus(data.error || '启动失败');
}
} catch (e) {
this.state.isMonitoring = false;
this.setStatus('启动UDP抓包失败: ' + e.message);
}
this.render();
}
async stopMonitor() {
this.setStatus('正在停止UDP抓包...');
try {
const res = await fetch('/api/udp-monitor/stop', { method: 'POST' });
const data = await res.json();
this.state.isMonitoring = false;
this.setStatus(data.message || 'UDP抓包已停止');
if (this.state.timer) {
clearInterval(this.state.timer);
this.state.timer = null;
}
} catch (e) {
this.setStatus('停止UDP抓包失败: ' + e.message);
}
this.render();
}
startPolling() {
if (this.state.timer) clearInterval(this.state.timer);
this.state.timer = setInterval(() => this.fetchData(), 1000);
}
async fetchData() {
if (!this.state.isMonitoring) return;
try {
const res = await fetch('/api/udp-monitor/data');
const data = await res.json();
if (data.success) {
if (Array.isArray(data.data) && data.data.length > 0) {
this.state.data = this.state.data.concat(data.data);
// 最多只保留1000条
if (this.state.data.length > 1000) {
this.state.data = this.state.data.slice(-1000);
}
this.renderDataList();
this.updateDetailContent();
}
this.state.isMonitoring = data.isMonitoring;
this.shadowRoot.getElementById('monitorStatus').textContent = data.isMonitoring ? '运行中' : '已停止';
}
} catch (e) {
this.setStatus('获取UDP数据失败: ' + e.message);
}
}
initialize() {
this.setStatus('请设置IP和端口后点击开始抓包');
// 检查当前监控状态
fetch('/api/udp-monitor/status').then(res => res.json()).then(data => {
if (data.success && data.isMonitoring) {
this.state.isMonitoring = true;
this.state.ip = data.ip || this.state.ip;
this.state.port = data.port || this.state.port;
this.setStatus('UDP抓包已在运行');
this.startPolling();
this.render();
}
});
}
reactivate() {
this.initialize();
}
selectDataItem(index) {
this.state.selectedData = index;
this.renderDataList();
this.updateDetailContent();
}
updateDetailContent() {
const detailContent = this.shadowRoot.getElementById('detailContent');
if (!detailContent) return;
if (this.state.selectedData === null || !this.state.data[this.state.selectedData]) {
detailContent.innerHTML = '<div class="no-selection">请从左侧选择一个数据包查看详情</div>';
return;
}
const item = this.state.data[this.state.selectedData];
detailContent.innerHTML = `
<div class="hex-content">${this.formatHexContent(item.data)}</div>
<div class="parse-content">
${this.formatParseContent(item.data)}
</div>
`;
}
getPacketIndex(packet) {
// 这里可以根据需要实现数据包序号逻辑
return Math.floor(Math.random() * 1000) + 1;
}
formatHexContent(data) {
// 如果数据是十六进制字符串,直接处理
if (typeof data === 'string' && /^[0-9a-fA-F]+$/.test(data)) {
const bytes = [];
for (let i = 0; i < data.length; i += 2) {
const byte = parseInt(data.substr(i, 2), 16);
bytes.push(byte);
}
// 分行显示每行16个字节
const bytesPerLine = 16;
const lines = [];
for (let i = 0; i < bytes.length; i += bytesPerLine) {
const lineBytes = bytes.slice(i, i + bytesPerLine);
const lineHex = lineBytes.map(byte => byte.toString(16).padStart(2, '0')).join(' ');
const offset = i.toString(16).padStart(4, '0').toUpperCase();
lines.push(`${offset}: ${lineHex}`);
}
return lines.join('\n');
}
return '无效数据格式';
}
formatParseContent(data) {
if (typeof data !== 'string' || !/^[0-9a-fA-F]+$/.test(data) || data.length < 14) {
return '<div class="parse-item">数据包长度不足,无法解析</div>';
}
const bytes = [];
for (let i = 0; i < data.length; i += 2) {
const byte = parseInt(data.substr(i, 2), 16);
bytes.push(byte);
}
// 解析数据包头
const header = bytes.slice(0, 6); // 前6个字节
const sizeBytes = bytes.slice(6, 8); // 第7-8个字节表示数据包大小
let parseResult = '<div style="margin-bottom: 12px;"><b style="color: #1976d2; font-size: 16px;">数据包头解析</b></div>';
// 1. a6-XNSim数据
parseResult += `<div class="parse-item"><b>1.</b> <span class="hex-value">${header[0].toString(16).padStart(2, '0')}</span> - XNSim数据</div>`;
// 2. c0-C909数据 c1-C919数据
const dataType = header[1];
let dataTypeDesc = '';
if (dataType === 0xc0) {
dataTypeDesc = 'C909数据';
} else if (dataType === 0xc1) {
dataTypeDesc = 'C919数据';
} else {
dataTypeDesc = `未知数据类型(${dataType.toString(16).padStart(2, '0')})`;
}
parseResult += `<div class="parse-item"><b>2.</b> <span class="hex-value">${dataType.toString(16).padStart(2, '0')}</span> - ${dataTypeDesc}</div>`;
// 3. ATA章节号
const ataChapter = header[2];
parseResult += `<div class="parse-item"><b>3.</b> <span class="hex-value">${ataChapter.toString(16).padStart(2, '0')}</span> - ATA章节号 (ATA${ataChapter.toString().padStart(2, '0')})</div>`;
// 4. 模型编号
const modelNumber = header[3];
parseResult += `<div class="parse-item"><b>4.</b> <span class="hex-value">${modelNumber.toString(16).padStart(2, '0')}</span> - 模型编号 (${modelNumber})</div>`;
// 5. 结构体类别
const structType = header[4];
let structTypeDesc = '';
switch (structType) {
case 0x00:
structTypeDesc = '输入结构体';
break;
case 0x01:
structTypeDesc = '输出结构体';
break;
case 0x02:
structTypeDesc = '心跳结构体';
break;
default:
structTypeDesc = `未知结构体类型(${structType.toString(16).padStart(2, '0')})`;
}
parseResult += `<div class="parse-item"><b>5.</b> <span class="hex-value">${structType.toString(16).padStart(2, '0')}</span> - 结构体类别 (${structTypeDesc})</div>`;
// 6. 数据传输方向
const direction = header[5];
let directionDesc = '';
switch (direction) {
case 0x00:
directionDesc = '输入';
break;
case 0x01:
directionDesc = '输出';
break;
default:
directionDesc = `未知方向(${direction.toString(16).padStart(2, '0')})`;
}
parseResult += `<div class="parse-item"><b>6.</b> <span class="hex-value">${direction.toString(16).padStart(2, '0')}</span> - 数据传输方向 (${directionDesc})</div>`;
// 7. 数据包大小
const packetSize = (sizeBytes[0] << 8) | sizeBytes[1];
parseResult += `<div class="parse-item"><b>7.</b> <span class="hex-value">${sizeBytes[0].toString(16).padStart(2, '0')} ${sizeBytes[1].toString(16).padStart(2, '0')}</span> - 数据包大小 (${packetSize} 字节)</div>`;
return parseResult;
}
}
customElements.define('network-monitor', NetworkMonitor);