2025-06-03 16:55:53 +08:00
|
|
|
|
/**
|
|
|
|
|
* @class DataMonitor
|
|
|
|
|
* @extends HTMLElement
|
|
|
|
|
* @description 数据监控组件的基础类
|
|
|
|
|
*/
|
2025-04-28 12:25:20 +08:00
|
|
|
|
class DataMonitor extends HTMLElement {
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
this.attachShadow({ mode: 'open' });
|
2025-06-03 16:55:53 +08:00
|
|
|
|
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() {
|
2025-06-03 16:55:53 +08:00
|
|
|
|
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
|
|
|
|
}
|
2025-06-03 16:55:53 +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
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +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
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
/**
|
|
|
|
|
* @description 加载接口数据
|
|
|
|
|
*/
|
|
|
|
|
async loadInterfaces() {
|
2025-04-28 16:41:21 +08:00
|
|
|
|
try {
|
2025-06-03 16:55:53 +08:00
|
|
|
|
const { configurationId } = this.getCurrentSelection();
|
|
|
|
|
if (!configurationId) {
|
|
|
|
|
console.warn('未找到配置ID');
|
2025-04-28 16:41:21 +08:00
|
|
|
|
return;
|
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
|
|
|
|
const response = await fetch(`/api/interface/list?systemName=XNSim&confID=${configurationId}`);
|
2025-04-28 16:41:21 +08:00
|
|
|
|
const data = await response.json();
|
2025-06-03 16:55:53 +08:00
|
|
|
|
this.interfaces = data;
|
|
|
|
|
this.filteredInterfaces = this.filterInterfaces(this.searchText);
|
|
|
|
|
this.render();
|
2025-04-28 16:41:21 +08:00
|
|
|
|
} catch (error) {
|
2025-06-03 16:55:53 +08:00
|
|
|
|
console.error('加载接口数据失败:', error);
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
/**
|
|
|
|
|
* @description 根据搜索文本过滤接口
|
|
|
|
|
* @param {string} searchText - 搜索文本
|
|
|
|
|
* @returns {Array} 过滤后的接口数据
|
|
|
|
|
*/
|
|
|
|
|
filterInterfaces(searchText) {
|
|
|
|
|
if (!searchText) return this.interfaces;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
|
2025-06-03 16:55:53 +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
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +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
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
// 清除之前的定时器
|
|
|
|
|
if (this.searchTimeout) {
|
|
|
|
|
clearTimeout(this.searchTimeout);
|
|
|
|
|
}
|
2025-04-28 16:41:21 +08:00
|
|
|
|
|
2025-06-03 16:55:53 +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
|
|
|
|
}
|
2025-06-03 16:55:53 +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
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +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
|
|
|
|
}
|
2025-06-03 16:55:53 +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
|
|
|
|
}
|
2025-06-03 16:55:53 +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
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +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
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
return color;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +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
|
|
|
|
}
|
2025-06-03 16:55:53 +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() {
|
2025-06-03 16:55:53 +08:00
|
|
|
|
// 计算所有列宽之和
|
|
|
|
|
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;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
gap: 16px;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.tree-container {
|
|
|
|
|
width: 300px;
|
|
|
|
|
border-right: 1px solid #e0e0e0;
|
|
|
|
|
padding-right: 16px;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
display: flex;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
flex-direction: column;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.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;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.content-area {
|
2025-04-28 16:41:21 +08:00
|
|
|
|
flex-grow: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
height: 100%;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
min-width: 0;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow: hidden;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +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;
|
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.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
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.table-header-fixed {
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
width: fit-content;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.table-header-fixed .monitor-table {
|
|
|
|
|
width: max-content;
|
|
|
|
|
min-width: 100%;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +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
|
|
|
|
}
|
2025-06-03 16:55:53 +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;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
vertical-align: middle;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +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
|
|
|
|
}
|
2025-06-03 16:55:53 +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
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.monitor-table th.th-resize-active::after {
|
|
|
|
|
background: #1890ff;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +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;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
justify-content: center;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +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
|
|
|
|
}
|
2025-06-03 16:55:53 +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
|
|
|
|
}
|
2025-06-03 16:55:53 +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;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
min-height: 120px;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
overflow: hidden;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.chart-title {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
margin-bottom: 8px;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.chart-container {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 120px;
|
|
|
|
|
position: relative;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
#monitor-chart {
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
height: 100% !important;
|
|
|
|
|
display: block;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +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;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
font-size: 13px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background 0.2s;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.action-btn:hover {
|
|
|
|
|
background: #e6f7ff;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.chart-btn.drawing {
|
|
|
|
|
background: #1890ff;
|
|
|
|
|
color: #fff;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +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;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
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">
|
2025-06-03 16:55:53 +08:00
|
|
|
|
<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>
|
2025-06-03 16:55:53 +08:00
|
|
|
|
<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>
|
|
|
|
|
`;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
|
|
|
|
// 搜索框事件
|
|
|
|
|
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
|
|
|
|
}
|
2025-06-03 16:55:53 +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-06-03 16:55:53 +08:00
|
|
|
|
};
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
customElements.define('data-monitor', DataMonitor);
|