XNSim/XNSimPortal/components/data-collection.js

1326 lines
54 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.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('B03062001: 未找到当前构型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('B03062002: 加载接口数据失败:', 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('B03062003: 网络错误');
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('B03062004: 网络错误');
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('B03062005: 删除文件失败');
}
const result = await response.json();
if (!result.success) {
throw new Error(result.message || 'B03062006: 删除文件失败');
}
}
// 清空脚本文件
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('B03062007: 上传失败');
}
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 = ['A03063008: '];
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 || 'B03062009: 上传失败');
}
} 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;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 16px;
height: 100%;
}
.right-panel {
flex: 1 1 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 0;
min-height: 200px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
margin-bottom: 16px;
}
.loading-spinner {
border: 6px solid #f3f3f3;
border-top: 6px solid #1890ff;
border-radius: 50%;
width: 48px;
height: 48px;
animation: spin 1s linear infinite;
margin-bottom: 12px;
}
@keyframes spin {
0% { transform: rotate(0deg);}
100% { transform: rotate(360deg);}
}
.loading-text {
color: #1890ff;
font-size: 16px;
font-weight: bold;
margin-top: 0;
}
.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: all 0.3s;
}
.action-btn:hover {
background: #40a9ff;
}
.action-btn:disabled {
background: #d9d9d9;
color: rgba(0, 0, 0, 0.25);
cursor: not-allowed;
box-shadow: none;
}
/* 卸载脚本按钮样式 */
.action-btn.unload {
background: #ff4d4f;
}
.action-btn.unload:hover {
background: #ff7875;
}
.action-btn.unload:disabled {
background: #d9d9d9;
color: rgba(0, 0, 0, 0.25);
}
/* 停止采集按钮样式 */
.action-btn.stop {
background: #faad14;
}
.action-btn.stop:hover {
background: #ffc53d;
}
.action-btn.stop:disabled {
background: #d9d9d9;
color: rgba(0, 0, 0, 0.25);
}
/* 图标按钮样式 */
.action-btn.icon-btn {
background: none;
border: none;
padding: 0 2px;
width: 28px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
margin-left: 0;
}
.action-btn.icon-btn:disabled {
opacity: 0.4;
}
.action-btn.icon-btn:hover:not(:disabled) {
background: #f0f0f0;
border-radius: 4px;
}
.input-row {
display: flex;
flex-direction: column;
gap: 8px;
}
.input-group {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
width: 100%;
}
.input-group > div {
display: flex;
align-items: center;
gap: 4px;
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-row-abs {
position: relative;
width: 100%;
display: flex;
align-items: center;
}
.input-box {
width: 100%;
padding-right: 36px;
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;
}
.input-unit {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
font-size: 16px;
color: #888;
white-space: nowrap;
}
.action-btn.icon-btn {
position: absolute;
right: 4px;
top: 50%;
transform: translateY(-50%);
margin-left: 0;
}
.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.collectStatus === 2 ? 'disabled' : ''}>${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">采集频率</div>
<div class="input-row-abs">
<input class="input-box" id="collectFreqInput" type="number" min="1" max="10000" value="${this.collectFreq !== undefined ? this.collectFreq : ''}" disabled />
<span class="input-unit">Hz</span>
</div>
</div>
<div class="input-group">
<div class="input-label">采集时长</div>
<div class="input-row-abs">
<input class="input-box" id="collectDurationInput" type="number" min="1" max="86400" value="${this.collectDuration !== undefined ? this.collectDuration : ''}" disabled />
<span class="input-unit">秒</span>
</div>
</div>
<div class="input-group">
<div class="input-label">输出文件</div>
<div class="input-row-abs">
<input class="input-box" id="outputFileInput" type="text" value="${this.outputFile || ''}" disabled />
<button class="action-btn icon-btn" id="downloadBtn" ${this.collectStatus === 3 ? '' : 'disabled'} title="下载">
<img src="./assets/icons/png/download_b.png" alt="下载" style="width:16px;height:16px;" />
</button>
</div>
</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" data-interfacename="${interfaceName}" data-modelstructname="${groupName}">
<span class="interface-name">${interfaceName}</span>
${size1 > 0 ? `<span class="array-size">[${size1}${size2 > 0 ? `][${size2}` : ''}]</span>` : ''}
</div>
`).join('')}
</div>
</div>
`).join('')}
</div>
</div>
</div>
<div class="right-panel">
${this.collectStatus === 2 ? `
<div class="loading-spinner"></div>
<div class="loading-text">数据采集中...</div>
` : ''}
</div>
</div>
`;
// 添加新的事件监听器
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(`B03062010: 读取文件失败: ${response.status} ${response.statusText}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.message || 'B03062011: 读取文件失败');
}
// 解析CSV数据
const csvData = result.data;
if (!csvData) {
throw new Error('A03062012: CSV数据为空');
}
const lines = csvData.split('\n');
if (lines.length < 2) {
throw new Error('A03062013: 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('B03062014: 加载采集数据失败:', error);
alert('加载采集数据失败: ' + error.message);
}
this.render();
} else {
throw new Error(result.message);
}
} catch (error) {
console.error('B03062015: 停止采集失败:', 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('B03062016: 启动采集失败:', 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('B03062017: 创建浮动窗口组件失败');
}
// 添加数据点计数器
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('B03062018: 下载文件失败:', error);
alert('下载文件失败: ' + error.message);
}
}
}
customElements.define('data-collection', DataCollection);