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.chartWindows = new Map(); // 存储打开的图表窗口 // 保存事件处理函数的引用 this.handleClick = (e) => { const btn = e.target.closest('#downloadBtn'); if (btn) { this.handleDownload(); return; } 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); } }; // 确保 FloatingChartWindow 组件已注册 if (!customElements.get('floating-chart-window')) { const script = document.createElement('script'); script.src = './components/FloatingChartWindow.js'; document.head.appendChild(script); } } 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 monitorRes = await fetch('/api/dds-monitor/status'); if (!monitorRes.ok) throw new Error('网络错误'); const monitorData = await monitorRes.json(); if (monitorData.isInitialized) { this.monitorStatus = 1; } else { this.monitorStatus = 0; } // 如果正在采集中,检查采集状态 if (this.collectStatus === 2) { const collectRes = await fetch('/api/data-collect/status'); if (!collectRes.ok) throw new Error('网络错误'); const collectData = await collectRes.json(); if (collectData.status === 0) { // 采集完成 // 模拟点击停止采集按钮 const startCollectBtn = this.shadowRoot.getElementById('startCollectBtn'); if (startCollectBtn) { startCollectBtn.click(); } } } this.updateMonitorStatus(); } catch (e) { this.monitorStatus = 2; } }, 1000); } disconnectedCallback() { this.isActive = false; // 清理所有图表窗口 this.chartWindows.forEach((window, windowId) => { // 触发关闭事件,确保所有清理工作都完成 window.dispatchEvent(new CustomEvent('close')); window.remove(); }); this.chartWindows.clear(); // 组件销毁时清理定时器 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 outputFileInput = this.shadowRoot.getElementById('outputFileInput'); const startCollectBtn = this.shadowRoot.getElementById('startCollectBtn'); const downloadBtn = this.shadowRoot.getElementById('downloadBtn'); scriptFileInput.value = ''; outputFileInput.value = ''; startCollectBtn.disabled = true; // 禁用下载按钮 if (downloadBtn) downloadBtn.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 = 100; // 默认值 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]); // 保存采集参数 this.collectDuration = duration; this.collectFreq = frequency; } } // 解析输出文件名 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; } if (collectDurationInput) { collectDurationInput.value = duration; } if (outputFileInput) { outputFileInput.value = outputFile; } if (startCollectBtn) { startCollectBtn.disabled = false; } // 更新按钮文本 const loadScriptBtn = this.shadowRoot.getElementById('loadScriptBtn'); loadScriptBtn.textContent = '卸载脚本'; // 存储结构体数据供后续使用 this.structData = structData; this.outputFile = outputFile; // 保存输出文件名 // 重新渲染组件以显示采集列表 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 = `
未监控
脚本文件
采集频率
Hz
采集时长
输出文件
采集列表:
${Object.entries(collectGroups).map(([groupName, interfaces]) => `
${groupName}
${Object.entries(interfaces).map(([interfaceName, [size1, size2]]) => `
${interfaceName} ${size1 > 0 ? `[${size1}${size2 > 0 ? `][${size2}` : ''}]` : ''}
`).join('')}
`).join('')}
${this.collectStatus === 2 ? `
数据采集中...
` : ''}
`; // 添加新的事件监听器 this.shadowRoot.addEventListener('click', this.handleClick); this.shadowRoot.addEventListener('change', this.handleChange); // 更新监控状态 this.updateMonitorStatus(); // 在树型控件部分添加双击事件处理 this.shadowRoot.querySelectorAll('.interface-item').forEach(itemEl => { itemEl.ondblclick = (e) => { const name = itemEl.getAttribute('data-interfacename'); const struct = itemEl.getAttribute('data-modelstructname'); this.handleInterfaceDblClick(name, struct); }; }); } // 重新激活组件 reactivate() { if (this.isActive) return; // 如果已经激活,直接返回 this.isActive = true; this.startStatusTimer(); } async loadCollectResult() { try { const response = await fetch('/api/filesystem/read', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename: 'collect_result.csv' }) }); if (!response.ok) { throw new Error(`读取文件失败: ${response.status} ${response.statusText}`); } const result = await response.json(); if (!result.success) { throw new Error(result.message || '读取文件失败'); } // 解析CSV数据 const csvData = result.data; if (!csvData) { throw new Error('CSV数据为空'); } const lines = csvData.split('\n'); if (lines.length < 2) { throw new Error('CSV文件格式错误:数据行数不足'); } // 获取接口名称(第一行)并解析数组索引 const interfaceNames = lines[0].split(',').map(name => { const trimmedName = name.trim(); // 匹配接口名称和数组索引 const match = trimmedName.match(/^(.+?)\((\d+)(?:_(\d+))?\)$/); if (match) { return { fullName: trimmedName, baseName: match[1], index1: parseInt(match[2], 10) - 1, // 转换为0基索引 index2: match[3] ? parseInt(match[3], 10) - 1 : null // 转换为0基索引 }; } return { fullName: trimmedName, baseName: trimmedName, index1: null, index2: null }; }); // 按接口名称整理数据 const organizedData = {}; // 处理数据行 for (let i = 1; i < lines.length; i++) { const values = lines[i].split(',').map(value => parseFloat(value.trim())); if (values.length === interfaceNames.length) { // 第一列是时间 const time = values[0]; interfaceNames.forEach(({ fullName, baseName, index1, index2 }, valueIndex) => { if (valueIndex === 0) return; // 跳过时间列 if (index1 !== null) { // 数组接口 if (!organizedData[baseName]) { organizedData[baseName] = { isArray: true, data: [], times: [] // 添加时间数组 }; } if (index2 !== null) { // 二维数组 if (!organizedData[baseName].data[index1]) { organizedData[baseName].data[index1] = []; } if (!organizedData[baseName].data[index1][index2]) { organizedData[baseName].data[index1][index2] = []; } organizedData[baseName].data[index1][index2].push(values[valueIndex]); } else { // 一维数组 if (!organizedData[baseName].data[index1]) { organizedData[baseName].data[index1] = []; } organizedData[baseName].data[index1].push(values[valueIndex]); } } else { // 非数组接口 if (!organizedData[fullName]) { organizedData[fullName] = { isArray: false, data: [], times: [] // 添加时间数组 }; } organizedData[fullName].data.push(values[valueIndex]); // 添加时间到对应的接口数据中 if (!organizedData[fullName].times.includes(time)) { organizedData[fullName].times.push(time); } } // 添加时间到数组接口数据中 if (index1 !== null && !organizedData[baseName].times.includes(time)) { organizedData[baseName].times.push(time); } }); } } return organizedData; } catch (error) { console.error('加载采集结果失败:', error); throw error; } } async handleStartCollect() { // 检查监控状态 if (this.monitorStatus !== 1) { return; } // 检查是否已加载脚本 if (!this.scriptFile) { alert('请先加载脚本'); return; } const startCollectBtn = this.shadowRoot.getElementById('startCollectBtn'); const loadScriptBtn = this.shadowRoot.getElementById('loadScriptBtn'); const downloadBtn = this.shadowRoot.getElementById('downloadBtn'); // 如果正在采集,则停止采集 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 = 3; // 改为采集完成状态 // 更新按钮状态 startCollectBtn.textContent = '开始采集'; startCollectBtn.disabled = false; startCollectBtn.classList.remove('stop'); // 启用卸载脚本按钮 loadScriptBtn.disabled = false; // 启用下载按钮 if (downloadBtn) downloadBtn.disabled = false; // 加载采集结果 try { const collectData = await this.loadCollectResult(); // 存储采集数据 this.collectData = collectData; } catch (error) { console.error('加载采集数据失败:', error); alert('加载采集数据失败: ' + error.message); } this.render(); } 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.chartWindows.forEach((window, windowId) => { window.dispatchEvent(new CustomEvent('close')); window.remove(); }); this.chartWindows.clear(); // 更新采集状态 this.collectStatus = 2; // 设置为采集中 // 更新按钮状态 startCollectBtn.textContent = '停止采集'; startCollectBtn.classList.add('stop'); // 禁用卸载脚本按钮 loadScriptBtn.disabled = true; // 禁用下载按钮 if (downloadBtn) downloadBtn.disabled = true; this.render(); } else { throw new Error(result.message); } } catch (error) { console.error('启动采集失败:', error); alert('启动采集失败: ' + error.message); } } async handleInterfaceDblClick(interfaceName, modelStructName) { // 只有在采集完成状态才允许绘图 if (this.collectStatus !== 3) { return; } // 检查是否已经存在该接口的图表窗口 const windowId = `${interfaceName}_${modelStructName}`; if (this.chartWindows.has(windowId)) { // 如果窗口已存在,将其置顶 const window = this.chartWindows.get(windowId); window.setAttribute('z-index', Date.now()); return; } try { // 获取左侧面板和工具栏的位置信息 const leftPanel = this.shadowRoot.querySelector('.left-panel'); const toolbar = this.shadowRoot.querySelector('.toolbar'); const leftPanelRect = leftPanel.getBoundingClientRect(); const toolbarRect = toolbar.getBoundingClientRect(); // 计算可用区域 const minX = leftPanelRect.right; const maxX = window.innerWidth; const minY = toolbarRect.bottom; const maxY = window.innerHeight; // 创建图表数据 const chartData = { labels: [], datasets: [] }; // 创建图表配置 const chartOptions = { responsive: true, maintainAspectRatio: false, animation: false, elements: { point: { radius: 0 }, line: { tension: 0 } }, scales: { y: { beginAtZero: false, display: true, ticks: { callback: function(value) { if (Math.abs(value) > 1000 || (Math.abs(value) < 0.1 && value !== 0)) { return value.toExponential(1); } return value.toFixed(1); }, maxTicksLimit: 8 } }, x: { display: true, type: 'linear', ticks: { callback: function(value) { return value.toFixed(1); } } } }, plugins: { legend: { display: true, position: 'top' }, tooltip: { enabled: true, mode: 'index', intersect: false, callbacks: { label: function(context) { console.log('Tooltip context:', context); console.log('Raw data:', context.raw); if (!context.raw) { console.log('No raw data'); return `${context.dataset.label}: N/A`; } if (typeof context.raw.y === 'undefined') { console.log('No y value in raw data'); return `${context.dataset.label}: N/A`; } const value = context.raw.y; console.log('Value type:', typeof value, 'Value:', value); if (typeof value !== 'number') { console.log('Value is not a number'); return `${context.dataset.label}: N/A`; } return `${context.dataset.label}: ${value.toFixed(1)}`; } } } } }; // 创建浮动窗口组件 const floatingWindow = document.createElement('floating-chart-window'); if (!floatingWindow) { throw new Error('创建浮动窗口组件失败'); } // 添加数据点计数器 floatingWindow.dataPointIndex = 0; // 先添加到页面 document.body.appendChild(floatingWindow); this.chartWindows.set(windowId, floatingWindow); // 设置浮动窗口的约束 floatingWindow.setAttribute('constraints', JSON.stringify({ minX: minX, maxX: maxX, minY: minY, maxY: maxY })); // 再设置属性 floatingWindow.setAttribute('title', interfaceName); floatingWindow.setAttribute('initial-position', JSON.stringify({ x: minX + 20, // 在左侧面板右侧留出20px的间距 y: minY + 20 // 在工具栏下方留出20px的间距 })); floatingWindow.setAttribute('initial-size', JSON.stringify({ width: 400, height: 300 })); floatingWindow.setAttribute('chart-data', JSON.stringify(chartData)); floatingWindow.setAttribute('chart-options', JSON.stringify(chartOptions)); // 更新图表数据 if (this.collectData && this.collectData[interfaceName]) { const interfaceData = this.collectData[interfaceName]; if (interfaceData.isArray) { // 数组接口 if (interfaceData.data[0] && Array.isArray(interfaceData.data[0][0])) { // 二维数组 const datasets = []; interfaceData.data.forEach((row, i) => { row.forEach((values, j) => { if (values && values.length > 0) { datasets.push({ label: `${interfaceName}[${i+1}][${j+1}]`, data: values, borderColor: `rgb(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)})`, tension: 0 }); } }); }); // 更新图表数据 floatingWindow.updateChartData({ labels: interfaceData.times, datasets: datasets }); } else { // 一维数组 const datasets = []; interfaceData.data.forEach((values, i) => { if (values && values.length > 0) { datasets.push({ label: `${interfaceName}[${i+1}]`, data: values, borderColor: `rgb(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)})`, tension: 0 }); } }); // 更新图表数据 floatingWindow.updateChartData({ labels: interfaceData.times, datasets: datasets }); } } else { // 非数组接口 if (interfaceData.data && interfaceData.data.length > 0) { // 更新图表数据 floatingWindow.updateChartData({ labels: interfaceData.times, datasets: [{ label: interfaceName, data: interfaceData.data, borderColor: 'rgb(75, 192, 192)', tension: 0 }] }); } } } // 添加关闭事件处理 floatingWindow.addEventListener('close', () => { this.chartWindows.delete(windowId); }); } catch (error) { console.error('创建图表窗口失败:', error); alert('创建图表窗口失败,请查看控制台了解详情'); } } async handleDownload() { if (!this.outputFile) { alert('没有可下载的文件'); return; } try { // 创建一个隐藏的a标签用于下载 const link = document.createElement('a'); link.href = `/api/filesystem/download?filename=${encodeURIComponent(this.outputFile)}`; link.download = this.outputFile; document.body.appendChild(link); link.click(); document.body.removeChild(link); } catch (error) { console.error('下载文件失败:', error); alert('下载文件失败: ' + error.message); } } } customElements.define('data-collection', DataCollection);