XNSim/XNSimHtml/components/data-monitor.js

769 lines
29 KiB
JavaScript
Raw 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 DataMonitor
* @extends HTMLElement
* @description 数据监控组件的基础类
*/
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;
}
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);
}
};
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;
}
}
/**
* @description 从localStorage获取当前选择的配置
* @returns {Object} 包含plane和configurationId的对象
*/
getCurrentSelection() {
const selection = localStorage.getItem('xnsim-selection');
return selection ? JSON.parse(selection) : { plane: '', configurationId: '' };
}
/**
* @description 加载接口数据
*/
async loadInterfaces() {
try {
const { configurationId } = this.getCurrentSelection();
if (!configurationId) {
console.warn('未找到配置ID');
return;
}
const response = await fetch(`/api/interface/list?systemName=XNSim&confID=${configurationId}`);
const data = await response.json();
this.interfaces = data;
this.filteredInterfaces = this.filterInterfaces(this.searchText);
this.render();
} catch (error) {
console.error('加载接口数据失败:', error);
}
}
/**
* @description 根据搜索文本过滤接口
* @param {string} searchText - 搜索文本
* @returns {Array} 过滤后的接口数据
*/
filterInterfaces(searchText) {
if (!searchText) return this.interfaces;
return this.interfaces.filter(item =>
item.InterfaceName.toLowerCase().includes(searchText.toLowerCase()) ||
item.ModelStructName.toLowerCase().includes(searchText.toLowerCase())
);
}
/**
* @description 处理搜索输入
* @param {Event} event - 输入事件
*/
handleSearch(event) {
this.searchText = event.target.value;
this.cursorPosition = event.target.selectionStart;
// 清除之前的定时器
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
// 设置新的定时器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);
}
/**
* @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();
}
}
/**
* @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('');
}
// 注入值输入限制
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;
}
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 }
}
}
});
}
/**
* @description 获取随机颜色
* @returns {string} 颜色字符串
*/
getRandomColor() {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
/**
* @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;
}
// 多条曲线
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');
}
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;
}, {});
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;
display: flex;
gap: 16px;
}
.tree-container {
width: 300px;
border-right: 1px solid #e0e0e0;
padding-right: 16px;
display: flex;
flex-direction: column;
}
.search-box {
margin-bottom: 16px;
}
.search-input {
width: 100%;
padding: 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
}
.tree-view {
flex-grow: 1;
overflow-y: auto;
}
.tree-group {
margin-bottom: 8px;
}
.group-header {
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;
}
.group-content.collapsed {
display: none;
}
.interface-item {
padding: 6px 8px;
cursor: pointer;
border-radius: 4px;
margin: 2px 0;
user-select: none;
}
.interface-item:hover {
background-color: #f0f0f0;
}
.content-area {
flex-grow: 1;
display: flex;
flex-direction: column;
height: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.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;
display: flex;
flex-direction: column;
}
.table-scroll-x {
width: 100%;
height: 100%;
overflow-x: auto;
overflow-y: hidden;
display: flex;
flex-direction: column;
}
.table-header-fixed {
overflow: hidden;
width: fit-content;
}
.table-header-fixed .monitor-table {
width: max-content;
min-width: 100%;
}
.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;
}
.cell-text {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
.monitor-table input,
.monitor-table button {
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.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;
}
.monitor-table th.th-resize-active::after {
background: #1890ff;
}
.divider {
height: 12px;
background: transparent;
cursor: row-resize;
width: 100%;
position: relative;
z-index: 2;
transition: background 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.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;
}
.divider:hover .divider-bar,
.divider.active .divider-bar {
background: #1890ff;
box-shadow: 0 2px 8px rgba(24,144,255,0.15);
}
.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;
display: flex;
flex-direction: column;
min-height: 120px;
min-width: 0;
overflow: hidden;
}
.chart-title {
font-size: 15px;
font-weight: bold;
margin-bottom: 8px;
}
.chart-container {
flex: 1;
min-height: 120px;
position: relative;
}
#monitor-chart {
width: 100% !important;
height: 100% !important;
display: block;
}
.action-btn {
margin: 0 2px;
padding: 3px 8px;
border: none;
border-radius: 4px;
background: #f0f0f0;
color: #333;
font-size: 13px;
cursor: pointer;
transition: background 0.2s;
}
.action-btn:hover {
background: #e6f7ff;
}
.chart-btn.drawing {
background: #1890ff;
color: #fff;
}
.th-resize {
position: absolute;
right: 0;
top: 0;
width: 12px;
height: 100%;
cursor: col-resize;
z-index: 10;
user-select: none;
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;
}
</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>
</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>
</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();
}
};
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 = '';
}
};
}
}
customElements.define('data-monitor', DataMonitor);