XNSim/XNSimHtml/components/data-monitor.js

769 lines
29 KiB
JavaScript
Raw Normal View History

/**
* @class DataMonitor
* @extends HTMLElement
* @description 数据监控组件的基础类
*/
2025-04-28 12:25:20 +08:00
class DataMonitor extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.interfaces = []; // 存储接口数据
this.filteredInterfaces = []; // 存储过滤后的接口数据
this.searchText = ''; // 搜索文本
this.searchTimeout = null; // 用于防抖的定时器
this.cursorPosition = 0; // 存储光标位置
this.tableData = []; // 表格数据,存储已添加的接口
this.chart = null; // Chart.js 实例
this.tableHeight = 220; // 表格初始高度(px)
this.isResizing = false; // 是否正在拖动分隔线
this.startY = 0; // 拖动起始Y
this.startTableHeight = 0; // 拖动起始表格高度
this.activeChartIndexes = []; // 当前绘图的行索引
// 列宽数组,初始为像素
this.colWidths = [160, 160, 120, 180, 320]; // px
this.dragColIndex = null;
this.dragStartX = 0;
this.dragStartWidth = 0;
this._colWidthInited = false;
this._resizeEventBinded = false;
2025-04-28 12:25:20 +08:00
}
connectedCallback() {
this.loadInterfaces();
// 延迟多次尝试,直到拿到有效宽度
const tryInitColWidth = (tryCount = 0) => {
const section = this.shadowRoot && this.shadowRoot.querySelector('.table-section');
if (section && section.clientWidth > 0) {
const totalWidth = section.clientWidth;
const ratios = [0.17, 0.17, 0.13, 0.19, 0.34];
this.colWidths = ratios.map(r => Math.floor(totalWidth * r));
this._colWidthInited = true;
this.render();
} else if (tryCount < 10) {
setTimeout(() => tryInitColWidth(tryCount + 1), 50);
2025-04-28 16:41:21 +08:00
}
};
tryInitColWidth();
// 只绑定一次全局拖动事件
if (!this._resizeEventBinded) {
window.addEventListener('mousemove', this._onWindowMouseMove = (e) => {
if (this.isResizing) {
const delta = e.clientY - this.startY;
let newHeight = this.startTableHeight + delta;
const minHeight = 60;
const maxHeight = Math.max(120, this.offsetHeight - 180);
if (newHeight < minHeight) newHeight = minHeight;
if (newHeight > maxHeight) newHeight = maxHeight;
this.tableHeight = newHeight;
this.render();
}
});
window.addEventListener('mouseup', this._onWindowMouseUp = () => {
if (this.isResizing) {
this.isResizing = false;
const divider = this.shadowRoot.querySelector('.divider');
if (divider) divider.classList.remove('active');
document.body.style.cursor = '';
document.body.style.userSelect = '';
}
});
this._resizeEventBinded = true;
2025-04-28 16:41:21 +08:00
}
}
/**
* @description 从localStorage获取当前选择的配置
* @returns {Object} 包含plane和configurationId的对象
*/
getCurrentSelection() {
const selection = localStorage.getItem('xnsim-selection');
return selection ? JSON.parse(selection) : { plane: '', configurationId: '' };
2025-04-28 16:41:21 +08:00
}
/**
* @description 加载接口数据
*/
async loadInterfaces() {
2025-04-28 16:41:21 +08:00
try {
const { configurationId } = this.getCurrentSelection();
if (!configurationId) {
console.warn('未找到配置ID');
2025-04-28 16:41:21 +08:00
return;
}
const response = await fetch(`/api/interface/list?systemName=XNSim&confID=${configurationId}`);
2025-04-28 16:41:21 +08:00
const data = await response.json();
this.interfaces = data;
this.filteredInterfaces = this.filterInterfaces(this.searchText);
this.render();
2025-04-28 16:41:21 +08:00
} catch (error) {
console.error('加载接口数据失败:', error);
2025-04-28 16:41:21 +08:00
}
}
/**
* @description 根据搜索文本过滤接口
* @param {string} searchText - 搜索文本
* @returns {Array} 过滤后的接口数据
*/
filterInterfaces(searchText) {
if (!searchText) return this.interfaces;
2025-04-28 16:41:21 +08:00
return this.interfaces.filter(item =>
item.InterfaceName.toLowerCase().includes(searchText.toLowerCase()) ||
item.ModelStructName.toLowerCase().includes(searchText.toLowerCase())
);
2025-04-28 16:41:21 +08:00
}
/**
* @description 处理搜索输入
* @param {Event} event - 输入事件
*/
handleSearch(event) {
this.searchText = event.target.value;
this.cursorPosition = event.target.selectionStart;
2025-04-28 16:41:21 +08:00
// 清除之前的定时器
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
2025-04-28 16:41:21 +08:00
// 设置新的定时器300ms后执行搜索
this.searchTimeout = setTimeout(() => {
this.filteredInterfaces = this.filterInterfaces(this.searchText);
this.render();
// 在下一个事件循环中恢复焦点和光标位置
requestAnimationFrame(() => {
const searchInput = this.shadowRoot.querySelector('.search-input');
if (searchInput) {
searchInput.focus();
searchInput.setSelectionRange(this.cursorPosition, this.cursorPosition);
}
});
}, 300);
2025-04-28 16:41:21 +08:00
}
/**
* @description 处理树节点双击事件将接口添加到表格
* @param {Object} item - 接口对象
*/
handleTreeItemDblClick(item) {
// 防止重复添加
if (!this.tableData.some(row => row.InterfaceName === item.InterfaceName && row.ModelStructName === item.ModelStructName)) {
this.tableData.push({
InterfaceName: item.InterfaceName,
ModelStructName: item.ModelStructName,
InjectValue: '',
Drawing: false,
color: this.getRandomColor()
});
this.renderTable();
2025-04-28 16:41:21 +08:00
}
}
/**
* @description 渲染表格内容
*/
renderTable() {
const tableBody = this.shadowRoot.querySelector('#monitor-table-body');
if (tableBody) {
tableBody.innerHTML = this.tableData.map((row, idx) => `
<tr>
<td><span class="cell-text">${row.InterfaceName}</span></td>
<td><span class="cell-text">${row.ModelStructName}</span></td>
<td><span class="cell-text"></span></td>
<td>
<input type="text" class="inject-input" data-index="${idx}" value="${row.InjectValue || ''}" placeholder="注入值" style="width:90px;" />
</td>
<td style="min-width:120px; white-space:nowrap;">
<button class="action-btn monitor-btn" data-index="${idx}">开始监控</button>
<button class="action-btn chart-btn${row.Drawing ? ' drawing' : ''}" data-index="${idx}">绘图</button>
<button class="action-btn inject-once-btn" data-index="${idx}">单次注入</button>
<button class="action-btn inject-loop-btn" data-index="${idx}">连续注入</button>
<button class="action-btn delete-btn" data-index="${idx}">删除</button>
</td>
</tr>
`).join('');
2025-04-28 16:41:21 +08:00
}
// 注入值输入限制
this.shadowRoot.querySelectorAll('.inject-input').forEach(input => {
input.oninput = (e) => {
// 只允许数字、负号、小数点、逗号
let v = e.target.value.replace(/[^0-9\-.,]/g, '');
e.target.value = v;
const idx = parseInt(e.target.getAttribute('data-index'));
this.tableData[idx].InjectValue = v;
};
});
// 按钮事件
this.shadowRoot.querySelectorAll('.chart-btn').forEach(btn => {
btn.onclick = (e) => {
const idx = parseInt(btn.getAttribute('data-index'));
// 多选支持
if (this.tableData[idx].Drawing) {
this.tableData[idx].Drawing = false;
} else {
this.tableData[idx].Drawing = true;
}
// 更新activeChartIndexes
this.activeChartIndexes = this.tableData.map((row, i) => row.Drawing ? i : null).filter(i => i !== null);
this.renderTable();
this.updateChart();
};
});
this.shadowRoot.querySelectorAll('.delete-btn').forEach(btn => {
btn.onclick = (e) => {
const idx = parseInt(btn.getAttribute('data-index'));
this.tableData.splice(idx, 1);
// 删除后同步更新activeChartIndexes
this.activeChartIndexes = this.tableData.map((row, i) => row.Drawing ? i : null).filter(i => i !== null);
this.renderTable();
this.updateChart();
};
});
}
/**
* @description 初始化图表
*/
initChart() {
const chartElement = this.shadowRoot.querySelector('#monitor-chart');
if (!chartElement) return;
if (typeof Chart === 'undefined') return;
if (this.chart) {
this.chart.destroy();
this.chart = null;
2025-04-28 16:41:21 +08:00
}
const ctx = chartElement.getContext('2d');
this.chart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: []
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: { beginAtZero: true }
},
animation: false,
plugins: {
legend: { display: true },
tooltip: { enabled: false }
}
}
});
2025-04-28 16:41:21 +08:00
}
/**
* @description 获取随机颜色
* @returns {string} 颜色字符串
*/
getRandomColor() {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
2025-04-28 16:41:21 +08:00
}
return color;
2025-04-28 16:41:21 +08:00
}
/**
* @description 更新图表支持多条曲线
*/
updateChart() {
if (!this.chart) return;
if (!this.activeChartIndexes || this.activeChartIndexes.length === 0) {
this.chart.data.labels = [];
this.chart.data.datasets = [];
this.chart.update('none');
return;
2025-04-28 16:41:21 +08:00
}
// 多条曲线
const labels = Array.from({length: 20}, (_, i) => i+1);
this.chart.data.labels = labels;
this.chart.data.datasets = this.activeChartIndexes.map(idx => {
const row = this.tableData[idx];
return {
label: row.InterfaceName,
data: Array.from({length: 20}, () => (Math.random()*100).toFixed(2)),
borderColor: row.color,
fill: false
};
});
this.chart.update('none');
2025-04-28 16:41:21 +08:00
}
2025-04-28 12:25:20 +08:00
render() {
// 计算所有列宽之和
const totalColWidth = this.colWidths.reduce((a, b) => a + b, 0);
let tableWidthStyle = '';
const section = this.shadowRoot && this.shadowRoot.querySelector('.table-section');
const containerWidth = section ? section.clientWidth : 940;
if (totalColWidth < containerWidth) {
tableWidthStyle = 'width:100%;';
} else {
tableWidthStyle = 'width:max-content;';
}
// 首次渲染时动态分配列宽已移至connectedCallback防止死循环
// 按ModelStructName分组
const groupedInterfaces = this.filteredInterfaces.reduce((groups, item) => {
const group = groups[item.ModelStructName] || [];
group.push(item);
groups[item.ModelStructName] = group;
return groups;
}, {});
2025-04-28 12:25:20 +08:00
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
height: 100%;
overflow: auto;
padding: 16px;
box-sizing: border-box;
}
.monitor-container {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 16px;
height: 100%;
box-sizing: border-box;
2025-04-28 16:41:21 +08:00
display: flex;
gap: 16px;
2025-04-28 12:25:20 +08:00
}
.tree-container {
width: 300px;
border-right: 1px solid #e0e0e0;
padding-right: 16px;
2025-04-28 12:25:20 +08:00
display: flex;
flex-direction: column;
2025-04-28 12:25:20 +08:00
}
.search-box {
margin-bottom: 16px;
}
.search-input {
width: 100%;
padding: 8px;
border: 1px solid #d9d9d9;
2025-04-28 16:41:21 +08:00
border-radius: 4px;
font-size: 14px;
}
.tree-view {
flex-grow: 1;
overflow-y: auto;
}
.tree-group {
margin-bottom: 8px;
}
.group-header {
2025-04-28 16:41:21 +08:00
font-weight: bold;
padding: 8px;
background-color: #f5f5f5;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.group-content {
margin-left: 20px;
display: block;
2025-04-28 16:41:21 +08:00
}
.group-content.collapsed {
display: none;
}
.interface-item {
padding: 6px 8px;
cursor: pointer;
border-radius: 4px;
margin: 2px 0;
user-select: none;
}
.interface-item:hover {
2025-04-28 16:41:21 +08:00
background-color: #f0f0f0;
}
.content-area {
2025-04-28 16:41:21 +08:00
flex-grow: 1;
display: flex;
flex-direction: column;
height: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
2025-04-28 16:41:21 +08:00
}
.table-section {
background: #fff;
border-radius: 8px 8px 0 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
padding: 0;
margin-bottom: 0;
overflow: hidden;
max-height: 70vh;
min-height: 60px;
height: ${this.tableHeight}px;
transition: height 0.1s;
2025-04-28 16:41:21 +08:00
display: flex;
flex-direction: column;
}
.table-scroll-x {
width: 100%;
height: 100%;
overflow-x: auto;
overflow-y: hidden;
display: flex;
flex-direction: column;
2025-04-28 16:41:21 +08:00
}
.table-header-fixed {
overflow: hidden;
width: fit-content;
2025-04-28 16:41:21 +08:00
}
.table-header-fixed .monitor-table {
width: max-content;
min-width: 100%;
2025-04-28 16:41:21 +08:00
}
.table-body-scroll {
flex: 1;
overflow-y: auto;
width: 100%;
overflow-x: hidden;
overflow-x: auto;
max-height: calc(100% - 1px);
}
.table-body-scroll .monitor-table {
width: max-content;
min-width: 100%;
}
.monitor-table {
width: max-content;
min-width: 100%;
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
min-width: unset;
}
.monitor-table th, .monitor-table td {
padding: 8px;
border-bottom: 1px solid #e0e0e0;
text-align: left;
background: #fff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
position: relative;
max-width: 0;
2025-04-28 16:41:21 +08:00
}
.cell-text {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
2025-04-28 16:41:21 +08:00
white-space: nowrap;
vertical-align: middle;
2025-04-28 16:41:21 +08:00
}
.monitor-table input,
.monitor-table button {
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
2025-04-28 16:41:21 +08:00
}
.monitor-table th {
background: #f5f5f5;
position: sticky;
top: 0;
z-index: 2;
position: relative;
}
/* 竖线:表头和内容区 */
.monitor-table td:not(:last-child)::after {
content: '';
position: absolute;
right: 0;
top: 20%;
width: 1px;
height: 60%;
background: #e0e0e0;
z-index: 1;
2025-04-28 16:41:21 +08:00
}
.monitor-table th.th-resize-active::after {
background: #1890ff;
2025-04-28 16:41:21 +08:00
}
.divider {
height: 12px;
background: transparent;
cursor: row-resize;
width: 100%;
position: relative;
z-index: 2;
transition: background 0.2s;
2025-04-28 16:41:21 +08:00
display: flex;
align-items: center;
justify-content: center;
2025-04-28 16:41:21 +08:00
}
.divider-bar {
width: 60px;
height: 5px;
background: #c0c4cc;
border-radius: 3px;
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
transition: background 0.2s, box-shadow 0.2s;
2025-04-28 16:41:21 +08:00
}
.divider:hover .divider-bar,
.divider.active .divider-bar {
background: #1890ff;
box-shadow: 0 2px 8px rgba(24,144,255,0.15);
2025-04-28 16:41:21 +08:00
}
.chart-section {
background: #fff;
border-radius: 0 0 8px 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
padding: 12px;
flex: 1;
2025-04-28 16:41:21 +08:00
display: flex;
flex-direction: column;
min-height: 120px;
min-width: 0;
overflow: hidden;
2025-04-28 16:41:21 +08:00
}
.chart-title {
font-size: 15px;
font-weight: bold;
margin-bottom: 8px;
2025-04-28 16:41:21 +08:00
}
.chart-container {
flex: 1;
min-height: 120px;
position: relative;
2025-04-28 16:41:21 +08:00
}
#monitor-chart {
width: 100% !important;
height: 100% !important;
display: block;
2025-04-28 16:41:21 +08:00
}
.action-btn {
margin: 0 2px;
padding: 3px 8px;
border: none;
border-radius: 4px;
background: #f0f0f0;
2025-04-28 12:25:20 +08:00
color: #333;
font-size: 13px;
cursor: pointer;
transition: background 0.2s;
2025-04-28 16:41:21 +08:00
}
.action-btn:hover {
background: #e6f7ff;
2025-04-28 16:41:21 +08:00
}
.chart-btn.drawing {
background: #1890ff;
color: #fff;
2025-04-28 16:41:21 +08:00
}
.th-resize {
position: absolute;
right: 0;
top: 0;
width: 12px;
height: 100%;
cursor: col-resize;
z-index: 10;
user-select: none;
2025-04-28 16:41:21 +08:00
display: flex;
align-items: center;
justify-content: center;
}
.th-resize::after {
content: '';
position: absolute;
right: 0;
top: 20%;
width: 1px;
height: 60%;
background: #e0e0e0;
border-radius: 2px;
transition: background 0.2s;
}
.th-resize:hover::after,
.th-resize.active::after {
background: #1890ff;
2025-04-28 12:25:20 +08:00
}
</style>
<div class="monitor-container">
<div class="tree-container">
<div class="search-box">
<input type="text"
class="search-input"
placeholder="搜索接口..."
value="${this.searchText}">
</div>
<div class="tree-view">
${Object.entries(groupedInterfaces).map(([groupName, items]) => `
<div class="tree-group">
<div class="group-header" onclick="this.parentElement.querySelector('.group-content').classList.toggle('collapsed')">
<span class="group-icon"></span>
${groupName}
</div>
<div class="group-content">
${items.map(item => `
<div class="interface-item" data-interfacename="${item.InterfaceName}" data-modelstructname="${item.ModelStructName}">
${item.InterfaceName}
</div>
`).join('')}
</div>
</div>
`).join('')}
</div>
2025-04-28 16:41:21 +08:00
</div>
<div class="content-area">
<div class="table-section">
<div class="table-header-fixed">
<table class="monitor-table" style="${tableWidthStyle}">
<colgroup>
<col style="width:${this.colWidths[0]}px">
<col style="width:${this.colWidths[1]}px">
<col style="width:${this.colWidths[2]}px">
<col style="width:${this.colWidths[3]}px">
<col style="width:${this.colWidths[4]}px">
</colgroup>
<thead>
<tr>
<th>接口名称<div class="th-resize" data-col="0"></div></th>
<th>结构体名<div class="th-resize" data-col="1"></div></th>
<th>数据<div class="th-resize" data-col="2"></div></th>
<th>注入值<div class="th-resize" data-col="3"></div></th>
<th>操作<div class="th-resize" data-col="4"></div></th>
</tr>
</thead>
</table>
</div>
<div class="table-body-scroll">
<table class="monitor-table" style="${tableWidthStyle}">
<colgroup>
<col style="width:${this.colWidths[0]}px">
<col style="width:${this.colWidths[1]}px">
<col style="width:${this.colWidths[2]}px">
<col style="width:${this.colWidths[3]}px">
<col style="width:${this.colWidths[4]}px">
</colgroup>
<tbody id="monitor-table-body">
<!-- 表格内容 -->
</tbody>
</table>
</div>
</div>
<div class="divider"><div class="divider-bar"></div></div>
<div class="chart-section">
<div class="chart-title">数据监控图表</div>
<div class="chart-container">
<canvas id="monitor-chart"></canvas>
</div>
</div>
2025-04-28 12:25:20 +08:00
</div>
</div>
`;
// 搜索框事件
const searchInput = this.shadowRoot.querySelector('.search-input');
if (searchInput) {
searchInput.addEventListener('input', (e) => this.handleSearch(e));
}
// 树节点双击事件
this.shadowRoot.querySelectorAll('.interface-item').forEach(itemEl => {
itemEl.ondblclick = (e) => {
const name = itemEl.getAttribute('data-interfacename');
const struct = itemEl.getAttribute('data-modelstructname');
this.handleTreeItemDblClick({ InterfaceName: name, ModelStructName: struct });
};
});
// 渲染表格内容
this.renderTable();
// 初始化图表
this.initChart();
// 初始绘图
this.updateChart();
// 分隔线拖动事件
const divider = this.shadowRoot.querySelector('.divider');
if (divider) {
divider.onmousedown = (e) => {
this.isResizing = true;
this.startY = e.clientY;
this.startTableHeight = this.tableHeight;
divider.classList.add('active');
document.body.style.cursor = 'row-resize';
document.body.style.userSelect = 'none';
};
}
// 列宽拖动事件
this.shadowRoot.querySelectorAll('.th-resize').forEach(handle => {
handle.onmousedown = (e) => {
e.preventDefault();
this.dragColIndex = parseInt(handle.getAttribute('data-col'));
this.dragStartX = e.clientX;
this.dragStartWidth = this.colWidths[this.dragColIndex];
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
handle.classList.add('active');
// 高亮竖线
const th = handle.parentElement;
if (th) th.classList.add('th-resize-active');
};
handle.onmouseup = () => {
handle.classList.remove('active');
const th = handle.parentElement;
if (th) th.classList.remove('th-resize-active');
};
});
window.onmousemove = (e) => {
if (this.dragColIndex !== null) {
const delta = e.clientX - this.dragStartX;
let newWidth = this.dragStartWidth + delta;
if (newWidth < 60) newWidth = 60;
this.colWidths[this.dragColIndex] = newWidth;
this.render();
2025-04-28 16:41:21 +08:00
}
};
window.onmouseup = () => {
if (this.dragColIndex !== null) {
// 移除所有th-resize的active
this.shadowRoot.querySelectorAll('.th-resize').forEach(h => h.classList.remove('active'));
// 移除所有th的高亮
this.shadowRoot.querySelectorAll('.monitor-table th').forEach(th => th.classList.remove('th-resize-active'));
this.dragColIndex = null;
document.body.style.cursor = '';
document.body.style.userSelect = '';
2025-04-28 16:41:21 +08:00
}
};
2025-04-28 12:25:20 +08:00
}
}
customElements.define('data-monitor', DataMonitor);