2025-05-29 15:43:17 +08:00
|
|
|
|
/**
|
|
|
|
|
* @class NetworkMonitor
|
2025-06-18 17:13:19 +08:00
|
|
|
|
* @description 网络监控组件,用于监控系统网络状态和UDP数据抓包
|
2025-05-29 15:43:17 +08:00
|
|
|
|
*/
|
|
|
|
|
class NetworkMonitor extends HTMLElement {
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
this.attachShadow({ mode: 'open' });
|
2025-06-18 17:13:19 +08:00
|
|
|
|
this.state = {
|
|
|
|
|
ip: '127.0.0.1',
|
|
|
|
|
port: 54321,
|
|
|
|
|
isMonitoring: false,
|
|
|
|
|
data: [],
|
|
|
|
|
timer: null,
|
|
|
|
|
statusMsg: '',
|
|
|
|
|
};
|
2025-05-29 15:43:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connectedCallback() {
|
|
|
|
|
this.render();
|
|
|
|
|
this.initialize();
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-18 17:13:19 +08:00
|
|
|
|
disconnectedCallback() {
|
|
|
|
|
if (this.state.timer) {
|
|
|
|
|
clearInterval(this.state.timer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-29 15:43:17 +08:00
|
|
|
|
render() {
|
|
|
|
|
this.shadowRoot.innerHTML = `
|
|
|
|
|
<style>
|
|
|
|
|
:host {
|
|
|
|
|
display: block;
|
2025-06-18 17:13:19 +08:00
|
|
|
|
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;
|
|
|
|
|
max-width: 600px;
|
|
|
|
|
}
|
|
|
|
|
.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 {
|
|
|
|
|
max-height: 340px;
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
.data-item:hover {
|
|
|
|
|
box-shadow: 0 2px 8px rgba(64,158,255,0.13);
|
|
|
|
|
}
|
|
|
|
|
.data-item b {
|
|
|
|
|
color: #1976d2;
|
|
|
|
|
}
|
|
|
|
|
.data-item pre {
|
|
|
|
|
background: #f3f7fa;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
padding: 4px 6px;
|
|
|
|
|
margin: 2px 0 0 0;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #333;
|
|
|
|
|
}
|
|
|
|
|
.nodata {
|
|
|
|
|
color: #bbb;
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 30px 0;
|
2025-05-29 15:43:17 +08:00
|
|
|
|
}
|
|
|
|
|
</style>
|
2025-06-18 17:13:19 +08:00
|
|
|
|
<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>
|
2025-05-29 15:43:17 +08:00
|
|
|
|
`;
|
2025-06-18 17:13:19 +08:00
|
|
|
|
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 => `
|
|
|
|
|
<div class="data-item">
|
|
|
|
|
<div><b>时间:</b> ${new Date(item.timestamp).toLocaleString()}</div>
|
|
|
|
|
<div><b>来源:</b> ${item.source}</div>
|
|
|
|
|
<div><b>内容:</b> <pre>${typeof item.data === 'object' ? JSON.stringify(item.data, null, 2) : item.data}</pre></div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.state.isMonitoring = data.isMonitoring;
|
|
|
|
|
this.shadowRoot.getElementById('monitorStatus').textContent = data.isMonitoring ? '运行中' : '已停止';
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
this.setStatus('获取UDP数据失败: ' + e.message);
|
|
|
|
|
}
|
2025-05-29 15:43:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initialize() {
|
2025-06-18 17:13:19 +08:00
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-05-29 15:43:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reactivate() {
|
2025-06-18 17:13:19 +08:00
|
|
|
|
this.initialize();
|
2025-05-29 15:43:17 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
customElements.define('network-monitor', NetworkMonitor);
|