XNSim/XNSimHtml/components/data-collection.js

804 lines
32 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 DataCollection extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.monitorStatus = 0; // 0-未监控1-监控中2-错误
this.statusTimer = null;
this.scriptFile = null; // 存储上传的脚本文件
this.structData = null; // 存储结构体数据
this.outputFile = null; // 存储输出文件名
this.collectStatus = 0; // 0-未加载脚本、1-已加载脚本、2-数据采集中、3-采集完成
this.isActive = false; // 组件是否激活
// 保存事件处理函数的引用
this.handleClick = (e) => {
if (e.target.id === 'loadScriptBtn') {
this.handleLoadScript();
} else if (e.target.id === 'startCollectBtn') {
this.handleStartCollect();
}
};
this.handleChange = (e) => {
if (e.target.id === 'fileInput') {
this.handleFileSelect(e);
}
};
}
getCurrentSelection() {
const selection = localStorage.getItem('xnsim-selection');
if (!selection) {
return { plane: '', configurationId: '', domainId: '' };
}
try {
const parsedSelection = JSON.parse(selection);
return {
plane: parsedSelection.plane || '',
configurationId: parsedSelection.configurationId || '',
domainId: parsedSelection.domainId || ''
};
} catch (error) {
return { plane: '', configurationId: '', domainId: '' };
}
}
async loadInterfaces() {
try {
const { configurationId } = this.getCurrentSelection();
if (!configurationId) {
console.warn('未找到配置ID');
this.interfaces = [];
return;
}
const response = await fetch(`/api/interface/list?systemName=XNSim&confID=${configurationId}`);
const data = await response.json();
this.interfaces = data;
} catch (error) {
console.error('加载接口数据失败:', error);
this.interfaces = [];
}
}
async connectedCallback() {
this.isActive = true;
await this.loadInterfaces();
this.render();
this.startStatusTimer();
}
startStatusTimer() {
if (this.statusTimer) {
clearInterval(this.statusTimer);
}
this.statusTimer = setInterval(async () => {
try {
const res = await fetch('/api/dds-monitor/status');
if (!res.ok) throw new Error('网络错误');
const data = await res.json();
if (data.isInitialized) {
this.monitorStatus = 1;
} else {
this.monitorStatus = 0;
}
} catch (e) {
this.monitorStatus = 2;
}
this.updateMonitorStatus();
}, 1000);
}
disconnectedCallback() {
this.isActive = false;
if (this.statusTimer) {
clearInterval(this.statusTimer);
this.statusTimer = null;
}
}
updateMonitorStatus() {
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
const statusText = this.shadowRoot.getElementById('statusText');
if (statusIndicator) {
statusIndicator.classList.remove('active', 'inactive', 'error');
switch (this.monitorStatus) {
case 1:
statusIndicator.classList.add('active');
break;
case 2:
statusIndicator.classList.add('error');
break;
default:
statusIndicator.classList.add('inactive');
}
}
if (statusText) {
switch (this.monitorStatus) {
case 1:
statusText.textContent = '监控中';
break;
case 2:
statusText.textContent = '监控错误';
break;
default:
statusText.textContent = '未监控';
}
}
}
handleLoadScript() {
if (this.scriptFile) {
// 如果已经加载了脚本,则执行卸载操作
this.unloadScript();
} else {
// 否则执行加载操作
const fileInput = this.shadowRoot.getElementById('fileInput');
fileInput.click();
}
}
async unloadScript() {
try {
if (this.scriptFile) {
// 调用后端API删除文件
const response = await fetch(`/api/filesystem/upload/${this.scriptFile.name}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('删除文件失败');
}
const result = await response.json();
if (!result.success) {
throw new Error(result.message || '删除文件失败');
}
}
// 清空脚本文件
this.scriptFile = null;
this.structData = null;
this.outputFile = null;
// 清空输入框
const scriptFileInput = this.shadowRoot.getElementById('scriptFileInput');
const collectFreqInput = this.shadowRoot.getElementById('collectFreqInput');
const collectDurationInput = this.shadowRoot.getElementById('collectDurationInput');
const outputFileInput = this.shadowRoot.getElementById('outputFileInput');
const startCollectBtn = this.shadowRoot.getElementById('startCollectBtn');
scriptFileInput.value = '';
collectFreqInput.value = '100';
collectDurationInput.value = '60';
outputFileInput.value = '';
// 启用输入框
collectFreqInput.disabled = false;
collectDurationInput.disabled = false;
outputFileInput.disabled = false;
startCollectBtn.disabled = true;
// 更新按钮文本
const loadScriptBtn = this.shadowRoot.getElementById('loadScriptBtn');
loadScriptBtn.textContent = '载入脚本';
// 重新渲染以清空采集列表
this.render();
} catch (error) {
console.error('卸载脚本失败:', error);
alert('卸载脚本失败: ' + error.message);
}
}
async handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
// 检查文件扩展名
if (!file.name.toLowerCase().endsWith('.dcs')) {
alert('请选择.dcs格式的文件');
return;
}
try {
// 创建FormData对象
const formData = new FormData();
formData.append('file', file);
// 发送文件到后端
const response = await fetch('/api/filesystem/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('上传失败');
}
const result = await response.json();
if (result.success) {
this.scriptFile = file;
const scriptFileInput = this.shadowRoot.getElementById('scriptFileInput');
scriptFileInput.value = file.name;
// 读取文件内容
const fileContent = await file.text();
const lines = fileContent.split('\n');
let duration = 60; // 默认值
let frequency = 60; // 默认值
let outputFile = 'collect_result.csv'; // 默认值
let structData = {}; // 存储结构体数据
let missingInterfaces = []; // 存储不存在的接口
let invalidInterfaces = []; // 存储无效的接口
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// 跳过注释行
if (line.startsWith('!')) continue;
// 解析采集列表
if (line.startsWith('define collect_list')) {
let listContent = '';
let j = i;
// 收集直到遇到结束双引号
while (j < lines.length) {
const nextLine = lines[j].trim();
listContent += nextLine + ' ';
if (nextLine.endsWith('"')) {
break;
}
j++;
}
// 提取双引号中的内容
const matches = listContent.match(/"([^"]*)"/);
if (matches && matches[1]) {
// 按'-'分割,并处理每个项目
const collectList = matches[1]
.split('-')
.map(item => item.trim())
.filter(item => item && !item.startsWith('!')); // 过滤掉空项和注释
// 验证每个接口
for (const interfaceName of collectList) {
// 提取接口名称和数组索引
const match = interfaceName.match(/^(.+?)\((\d+)(?:_(\d+))?\)$/);
if (!match) {
// 如果不是数组格式,直接检查接口是否存在
const interfaceInfo = this.interfaces.find(item =>
item.InterfaceName === interfaceName
);
if (!interfaceInfo) {
missingInterfaces.push(interfaceName);
} else {
// 添加到结构体数据
if (!structData[interfaceInfo.ModelStructName]) {
structData[interfaceInfo.ModelStructName] = {};
}
structData[interfaceInfo.ModelStructName][interfaceName] = [0, 0];
}
} else {
// 处理数组格式的接口
const baseInterfaceName = match[1];
const index1 = match[2] ? parseInt(match[2], 10) - 1 : null;
const index2 = match[3] ? parseInt(match[3], 10) - 1 : null;
const interfaceInfo = this.interfaces.find(item =>
item.InterfaceName === baseInterfaceName
);
if (!interfaceInfo) {
missingInterfaces.push(interfaceName);
} else {
// 检查数组索引是否越界
if (index1 !== null) {
if (index2 !== null) {
// 二维数组
if (index1 >= interfaceInfo.InterfaceArraySize_1 ||
index2 >= interfaceInfo.InterfaceArraySize_2 ||
index1 < 0 || index2 < 0) {
invalidInterfaces.push(`${interfaceName} 的数组索引超出范围`);
continue;
}
} else {
// 一维数组
if (index1 >= interfaceInfo.InterfaceArraySize_1 || index1 < 0) {
invalidInterfaces.push(`${interfaceName} 的数组索引超出范围`);
continue;
}
}
}
// 添加到结构体数据
if (!structData[interfaceInfo.ModelStructName]) {
structData[interfaceInfo.ModelStructName] = {};
}
if (!structData[interfaceInfo.ModelStructName][baseInterfaceName]) {
structData[interfaceInfo.ModelStructName][baseInterfaceName] = [
interfaceInfo.InterfaceArraySize_1 || 0,
interfaceInfo.InterfaceArraySize_2 || 0
];
}
}
}
}
}
}
// 解析采集时长和频率
if (line.startsWith('for')) {
const matches = line.match(/for\s+(\d+)\s+at\s+(\d+)/);
if (matches) {
duration = parseInt(matches[1]);
frequency = parseInt(matches[2]);
}
}
// 解析输出文件名
if (line.startsWith('put/extend/all result')) {
const matches = line.match(/result\s+(\w+)/);
if (matches) {
outputFile = matches[1] + '.csv';
}
}
}
// 检查是否有错误
if (missingInterfaces.length > 0 || invalidInterfaces.length > 0) {
const errorMessages = [];
if (missingInterfaces.length > 0) {
errorMessages.push(`以下接口在系统中不存在:\n${missingInterfaces.join('\n')}`);
}
if (invalidInterfaces.length > 0) {
errorMessages.push(`以下接口的数组索引无效:\n${invalidInterfaces.join('\n')}`);
}
throw new Error(errorMessages.join('\n\n'));
}
// 更新UI
const collectFreqInput = this.shadowRoot.getElementById('collectFreqInput');
const collectDurationInput = this.shadowRoot.getElementById('collectDurationInput');
const outputFileInput = this.shadowRoot.getElementById('outputFileInput');
const startCollectBtn = this.shadowRoot.getElementById('startCollectBtn');
if (collectFreqInput) {
collectFreqInput.value = frequency;
collectFreqInput.disabled = true;
}
if (collectDurationInput) {
collectDurationInput.value = duration;
collectDurationInput.disabled = true;
}
if (outputFileInput) {
outputFileInput.value = outputFile;
outputFileInput.disabled = true;
}
if (startCollectBtn) {
startCollectBtn.disabled = false;
}
// 更新按钮文本
const loadScriptBtn = this.shadowRoot.getElementById('loadScriptBtn');
loadScriptBtn.textContent = '卸载脚本';
// 存储结构体数据供后续使用
this.structData = structData;
this.outputFile = outputFile; // 保存输出文件名
console.log('文件解析成功:', {
duration,
frequency,
outputFile,
structData
});
// 重新渲染组件以显示采集列表
this.render();
} else {
throw new Error(result.message || '上传失败');
}
} catch (error) {
console.error('上传文件失败:', error);
alert('上传文件失败: ' + error.message);
}
}
render() {
// 移除旧的事件监听器
this.shadowRoot.removeEventListener('click', this.handleClick);
this.shadowRoot.removeEventListener('change', this.handleChange);
// 树型控件分组
const groupedInterfaces = (this.interfaces || []).reduce((groups, item) => {
const group = groups[item.ModelStructName] || [];
group.push(item);
groups[item.ModelStructName] = group;
return groups;
}, {});
// 获取结构体数据中的接口
const collectGroups = this.structData || {};
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
height: 100%;
overflow: auto;
padding: 16px;
box-sizing: border-box;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
margin-bottom: 16px;
}
.toolbar-left {
display: flex;
align-items: center;
gap: 12px;
}
.monitor-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: #ccc;
}
.status-indicator.active {
background: #52c41a;
}
.status-indicator.error {
background: #ff4d4f;
}
.status-indicator.inactive {
background: #d9d9d9;
}
.main-container {
display: flex;
height: calc(100% - 56px);
gap: 16px;
}
.left-panel {
width: 320px;
min-width: 280px;
display: flex;
flex-direction: column;
gap: 16px;
height: 100%;
}
.panel-section {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.button-row {
display: flex;
gap: 12px;
}
.action-btn {
flex: 1;
padding: 8px 0;
border: none;
border-radius: 4px;
background: #1890ff;
color: #fff;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.action-btn:hover {
background: #40a9ff;
}
.action-btn:disabled {
background: #d9d9d9;
cursor: not-allowed;
}
/* 卸载脚本按钮样式 */
.action-btn.unload {
background: #ff4d4f;
}
.action-btn.unload:hover {
background: #ff7875;
}
/* 停止采集按钮样式 */
.action-btn.stop {
background: #faad14;
}
.action-btn.stop:hover {
background: #ffc53d;
}
.input-row {
display: flex;
flex-direction: column;
gap: 8px;
}
.input-group {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
width: 100%;
}
.input-label {
font-size: 13px;
color: #666;
min-width: 90px;
max-width: 90px;
flex-shrink: 0;
text-align: right;
line-height: 32px;
height: 32px;
display: flex;
align-items: center;
}
.input-box {
flex: 1;
width: 100%;
padding: 0 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
height: 32px;
line-height: 32px;
display: flex;
align-items: center;
}
.input-box[readonly] {
background: #f5f5f5;
color: #aaa;
cursor: not-allowed;
}
.input-box:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
.tree-section {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
padding: 16px;
flex: 1 1 0;
min-height: 120px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.tree-view {
width: 100%;
flex: 1 1 0;
overflow-y: auto;
}
.tree-group {
margin-bottom: 8px;
width: 100%;
}
.group-header {
font-weight: bold;
padding: 8px;
background-color: #f5f5f5;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
width: 100%;
box-sizing: border-box;
}
.group-content {
margin-left: 20px;
display: block;
width: calc(100% - 20px);
}
.group-content.collapsed {
display: none;
}
.interface-item {
padding: 6px 8px;
border-radius: 4px;
margin: 2px 0;
user-select: none;
width: 100%;
box-sizing: border-box;
color: #333;
display: flex;
align-items: center;
gap: 8px;
}
.interface-item:hover {
background-color: #f0f0f0;
}
.interface-item.selected {
background-color: #e6f7ff;
}
.interface-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.array-size {
font-size: 12px;
color: #666;
background: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
}
.collection-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;
}
.collection-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
</style>
<input type="file" id="fileInput" accept=".dcs" style="display: none;" />
<div class="toolbar">
<div class="toolbar-left">
<div class="monitor-status">
<div class="status-indicator" id="statusIndicator"></div>
<span id="statusText">未监控</span>
</div>
</div>
</div>
<div class="main-container">
<div class="left-panel">
<div class="panel-section">
<div class="button-row">
<button class="action-btn ${this.scriptFile ? 'unload' : ''}" id="loadScriptBtn">${this.scriptFile ? '卸载脚本' : '载入脚本'}</button>
<button class="action-btn ${this.collectStatus === 2 ? 'stop' : ''}" id="startCollectBtn" ${!this.scriptFile ? 'disabled' : ''}>${this.collectStatus === 2 ? '停止采集' : '开始采集'}</button>
</div>
<div class="input-row">
<div class="input-group">
<div class="input-label">脚本文件</div>
<input class="input-box" id="scriptFileInput" type="text" value="${this.scriptFile ? this.scriptFile.name : ''}" readonly />
</div>
<div class="input-group">
<div class="input-label">采集频率 (Hz)</div>
<input class="input-box" id="collectFreqInput" type="number" min="1" max="10000" value="100" ${this.scriptFile ? 'disabled' : ''} />
</div>
<div class="input-group">
<div class="input-label">采集时长 (秒)</div>
<input class="input-box" id="collectDurationInput" type="number" min="1" max="86400" value="60" ${this.scriptFile ? 'disabled' : ''} />
</div>
<div class="input-group">
<div class="input-label">输出文件</div>
<input class="input-box" id="outputFileInput" type="text" value="${this.outputFile || ''}" ${this.scriptFile ? 'disabled' : ''} />
</div>
</div>
</div>
<div class="tree-section">
<div style="font-size:14px;color:#333;font-weight:bold;margin-bottom:8px;">采集列表:</div>
<div class="tree-view">
${Object.entries(collectGroups).map(([groupName, interfaces]) => `
<div class="tree-group">
<div class="group-header" onclick="this.parentElement.querySelector('.group-content').classList.toggle('collapsed')">
<span>${groupName}</span>
</div>
<div class="group-content">
${Object.entries(interfaces).map(([interfaceName, [size1, size2]]) => `
<div class="interface-item">
<span class="interface-name">${interfaceName}</span>
</div>
`).join('')}
</div>
</div>
`).join('')}
</div>
</div>
</div>
</div>
`;
// 添加新的事件监听器
this.shadowRoot.addEventListener('click', this.handleClick);
this.shadowRoot.addEventListener('change', this.handleChange);
// 更新监控状态
this.updateMonitorStatus();
}
// 重新激活组件
reactivate() {
if (this.isActive) return; // 如果已经激活,直接返回
this.isActive = true;
this.startStatusTimer();
}
async handleStartCollect() {
// 检查监控状态
if (this.monitorStatus !== 1) {
alert('请先启动监控');
return;
}
// 检查是否已加载脚本
if (!this.scriptFile) {
alert('请先加载脚本');
return;
}
const startCollectBtn = this.shadowRoot.getElementById('startCollectBtn');
// 如果正在采集,则停止采集
if (this.collectStatus === 2) {
try {
const response = await fetch('/api/data-collect/stop', {
method: 'POST'
});
const result = await response.json();
if (result.success) {
// 更新采集状态
this.collectStatus = 1; // 改为已加载脚本状态
// 更新按钮状态
startCollectBtn.textContent = '开始采集';
startCollectBtn.disabled = false;
startCollectBtn.classList.remove('stop');
} else {
throw new Error(result.message);
}
} catch (error) {
console.error('停止采集失败:', error);
alert('停止采集失败: ' + error.message);
}
return;
}
// 开始采集
try {
// 调用后端接口启动采集
const response = await fetch('/api/data-collect/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
collectDataInfo: JSON.stringify(this.structData),
dcsFilePath: this.scriptFile.name
})
});
const result = await response.json();
if (result.success) {
// 更新采集状态
this.collectStatus = 2; // 设置为采集中
// 更新按钮状态
startCollectBtn.textContent = '停止采集';
startCollectBtn.classList.add('stop');
} else {
throw new Error(result.message);
}
} catch (error) {
console.error('启动采集失败:', error);
alert('启动采集失败: ' + error.message);
}
}
}
customElements.define('data-collection', DataCollection);