V0.21.15.250612_alpha:数据监控页面添加CSV文件上传
This commit is contained in:
parent
9fd0d530c4
commit
fc46cee50f
3
Release/.gitignore
vendored
Normal file
3
Release/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
log/*
|
||||
testData/*
|
||||
Packages/*
|
Binary file not shown.
@ -46,6 +46,8 @@ add_library(XNMonitorServer SHARED
|
||||
DataMonitorFactory.cpp
|
||||
DataInjectThread.h
|
||||
DataInjectThread.cpp
|
||||
CSVDataInjectThread.h
|
||||
CSVDataInjectThread.cpp
|
||||
)
|
||||
|
||||
# 添加头文件搜索路径
|
||||
|
148
XNMonitorServer/CSVDataInjectThread.cpp
Normal file
148
XNMonitorServer/CSVDataInjectThread.cpp
Normal file
@ -0,0 +1,148 @@
|
||||
#include "CSVDataInjectThread.h"
|
||||
#include "DataMonitorFactory.h"
|
||||
|
||||
CSVDataInjectThread::CSVDataInjectThread(std::string csvFilePath)
|
||||
: m_csvFilePath(csvFilePath), m_running(false), m_nextExecuteTime(0)
|
||||
{
|
||||
}
|
||||
|
||||
CSVDataInjectThread::~CSVDataInjectThread()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
bool CSVDataInjectThread::Initialize(std::vector<std::string> structNames)
|
||||
{
|
||||
m_csvFile.open(m_csvFilePath);
|
||||
if (!m_csvFile.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string headerLine;
|
||||
if (!std::getline(m_csvFile, headerLine)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::string> interfaceNames;
|
||||
std::stringstream ss(headerLine);
|
||||
std::string field;
|
||||
std::getline(ss, field, ',');
|
||||
while (std::getline(ss, field, ',')) {
|
||||
interfaceNames.push_back(field);
|
||||
}
|
||||
|
||||
// 将结构体和接口名称一一对应
|
||||
if (structNames.size() != interfaceNames.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < structNames.size(); i++) {
|
||||
m_structInterfaceMap[structNames[i]].push_back(interfaceNames[i]);
|
||||
}
|
||||
|
||||
for (const auto &[structName, interfaceNames] : m_structInterfaceMap) {
|
||||
auto dataMonitor = DataMonitorFactory::GetInstance(structName);
|
||||
if (dataMonitor == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (dataMonitor->isInitialized()) {
|
||||
m_alreadyStartedMonitors[structName] = dataMonitor;
|
||||
} else {
|
||||
m_notStartedMonitors[structName] = dataMonitor;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void CSVDataInjectThread::start()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (!m_running) {
|
||||
m_running = true;
|
||||
m_thread = std::thread(&CSVDataInjectThread::threadFunc, this);
|
||||
}
|
||||
}
|
||||
|
||||
void CSVDataInjectThread::stop()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_running) {
|
||||
m_running = false;
|
||||
m_cv.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
if (m_thread.joinable()) {
|
||||
m_thread.join();
|
||||
}
|
||||
|
||||
// 关闭文件
|
||||
if (m_csvFile.is_open()) {
|
||||
m_csvFile.close();
|
||||
}
|
||||
|
||||
// 释放未启动的监控器
|
||||
for (const auto &[structName, dataMonitor] : m_notStartedMonitors) {
|
||||
DataMonitorFactory::ReleaseInstance(structName);
|
||||
}
|
||||
m_notStartedMonitors.clear();
|
||||
m_alreadyStartedMonitors.clear();
|
||||
}
|
||||
|
||||
void CSVDataInjectThread::updateData()
|
||||
{
|
||||
// 读取下一行数据
|
||||
std::string line;
|
||||
if (!std::getline(m_csvFile, line)) {
|
||||
// 文件读取完毕,停止线程
|
||||
m_running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析数据
|
||||
std::stringstream ss(line);
|
||||
std::string field;
|
||||
std::getline(ss, field, ',');
|
||||
double timeStamp = std::stod(field);
|
||||
m_nextExecuteTime = static_cast<int64_t>(timeStamp * 1000); // 转换为毫秒
|
||||
|
||||
// 解析每个结构体的数据
|
||||
for (const auto &[structName, interfaceNames] : m_structInterfaceMap) {
|
||||
std::unordered_map<std::string, std::string> dataMap;
|
||||
for (const auto &interfaceName : interfaceNames) {
|
||||
std::getline(ss, field, ',');
|
||||
dataMap[interfaceName] = field;
|
||||
}
|
||||
m_data[structName] = dataMap;
|
||||
}
|
||||
}
|
||||
|
||||
void CSVDataInjectThread::threadFunc()
|
||||
{
|
||||
// 读取第一行数据
|
||||
updateData();
|
||||
|
||||
while (m_running) {
|
||||
int64_t nextTime = m_nextExecuteTime;
|
||||
|
||||
// 等待直到到达执行时间
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto targetTime = std::chrono::system_clock::from_time_t(nextTime / 1000)
|
||||
+ std::chrono::milliseconds(nextTime % 1000);
|
||||
|
||||
if (now < targetTime) {
|
||||
std::this_thread::sleep_until(targetTime);
|
||||
}
|
||||
|
||||
// 执行数据注入
|
||||
for (const auto &[structName, dataMonitor] : m_alreadyStartedMonitors) {
|
||||
if (dataMonitor && m_data.find(structName) != m_data.end()) {
|
||||
dataMonitor->setDataByString(m_data[structName]);
|
||||
}
|
||||
}
|
||||
|
||||
// 读取下一行数据
|
||||
updateData();
|
||||
}
|
||||
}
|
68
XNMonitorServer/CSVDataInjectThread.h
Normal file
68
XNMonitorServer/CSVDataInjectThread.h
Normal file
@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include "DataMonitor.h"
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <fstream>
|
||||
|
||||
/**
|
||||
* @brief 数据注入线程类,用于持续向数据监控器注入数据
|
||||
*/
|
||||
class CSVDataInjectThread
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief 构造函数
|
||||
* @param csvFilePath CSV文件路径
|
||||
*/
|
||||
CSVDataInjectThread(std::string csvFilePath);
|
||||
|
||||
/**
|
||||
* @brief 析构函数
|
||||
*/
|
||||
~CSVDataInjectThread();
|
||||
|
||||
bool Initialize(std::vector<std::string> structNames);
|
||||
|
||||
/**
|
||||
* @brief 启动数据注入线程
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* @brief 停止数据注入线程
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* @brief 从CSV文件读取下一行数据并更新执行时间
|
||||
* 如果文件读取完毕,将停止线程
|
||||
*/
|
||||
void updateData();
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 线程执行函数
|
||||
*/
|
||||
void threadFunc();
|
||||
|
||||
private:
|
||||
std::string m_csvFilePath;
|
||||
std::ifstream m_csvFile;
|
||||
std::unordered_map<std::string, std::vector<std::string>> m_structInterfaceMap;
|
||||
std::thread m_thread; ///< 数据注入线程
|
||||
std::atomic<bool> m_running; ///< 线程运行标志
|
||||
std::mutex m_mutex; ///< 互斥锁
|
||||
std::condition_variable m_cv; ///< 条件变量
|
||||
std::unordered_map<std::string, DataMonitorBasePtr>
|
||||
m_alreadyStartedMonitors; ///< 已经启动的数据监控器
|
||||
std::unordered_map<std::string, DataMonitorBasePtr>
|
||||
m_notStartedMonitors; ///< 未启动的数据监控器
|
||||
std::unordered_map<std::string, std::unordered_map<std::string, std::string>>
|
||||
m_data; ///< 要注入的数据
|
||||
std::atomic<int64_t> m_nextExecuteTime; ///< 下一次执行的时间点
|
||||
};
|
||||
|
||||
using CSVDataInjectThreadPtr = std::shared_ptr<CSVDataInjectThread>;
|
@ -8,6 +8,7 @@
|
||||
#include "SystemControl.h"
|
||||
#include "DataMonitorFactory.h"
|
||||
#include "DataInjectThread.h"
|
||||
#include "CSVDataInjectThread.h"
|
||||
|
||||
// 全局变量
|
||||
static bool g_initialized = false;
|
||||
@ -19,6 +20,8 @@ bool g_modelInfoMonitorStarted = false;
|
||||
SystemControl *systemControl = nullptr;
|
||||
bool g_systemControlStarted = false;
|
||||
|
||||
CSVDataInjectThreadPtr g_csvDataInjectThread;
|
||||
|
||||
// 初始化函数实现
|
||||
int XN_Initialize(const char *domainId, int domainIdLen, char *errorMsg, int errorMsgSize)
|
||||
{
|
||||
@ -587,8 +590,79 @@ int XNMONITORSERVER_EXPORT XN_InjectDataInterfaceFromCsv(const char *structName,
|
||||
const int csvFilePathLen, char *infoMsg,
|
||||
int infoMsgSize)
|
||||
{
|
||||
// TODO: 从csv文件中注入数据接口
|
||||
return -1;
|
||||
std::vector<std::string> structNames;
|
||||
std::string structNameStr(structName, structNameLen);
|
||||
try {
|
||||
nlohmann::json structNamesJson = nlohmann::json::parse(structNameStr);
|
||||
if (!structNamesJson.is_array()) {
|
||||
if (infoMsg && infoMsgSize > 0) {
|
||||
strncpy(infoMsg, "Invalid struct name format - expected JSON array",
|
||||
infoMsgSize - 1);
|
||||
infoMsg[infoMsgSize - 1] = '\0';
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
for (const auto &structNameJson : structNamesJson) {
|
||||
structNames.push_back(structNameJson.get<std::string>());
|
||||
}
|
||||
} catch (const nlohmann::json::parse_error &e) {
|
||||
if (infoMsg && infoMsgSize > 0) {
|
||||
strncpy(infoMsg, "Invalid JSON format", infoMsgSize - 1);
|
||||
infoMsg[infoMsgSize - 1] = '\0';
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::string csvFilePathStr(csvFilePath, csvFilePathLen);
|
||||
if (csvFilePathStr.empty()) {
|
||||
if (infoMsg && infoMsgSize > 0) {
|
||||
strncpy(infoMsg, "CSV 文件路径为空", infoMsgSize - 1);
|
||||
infoMsg[infoMsgSize - 1] = '\0';
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (!std::filesystem::exists(csvFilePathStr)) {
|
||||
if (infoMsg && infoMsgSize > 0) {
|
||||
strncpy(infoMsg, "CSV 文件不存在", infoMsgSize - 1);
|
||||
infoMsg[infoMsgSize - 1] = '\0';
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (g_csvDataInjectThread == nullptr) {
|
||||
g_csvDataInjectThread = std::make_shared<CSVDataInjectThread>(csvFilePathStr);
|
||||
bool ret = g_csvDataInjectThread->Initialize(structNames);
|
||||
if (ret) {
|
||||
g_csvDataInjectThread->start();
|
||||
} else {
|
||||
if (infoMsg && infoMsgSize > 0) {
|
||||
strncpy(infoMsg, "CSV 注入线程初始化失败", infoMsgSize - 1);
|
||||
infoMsg[infoMsgSize - 1] = '\0';
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
if (infoMsg && infoMsgSize > 0) {
|
||||
strncpy(infoMsg, "CSV 注入线程已在运行", infoMsgSize - 1);
|
||||
infoMsg[infoMsgSize - 1] = '\0';
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int XN_StopCsvDataInject(char *infoMsg, int infoMsgSize)
|
||||
{
|
||||
if (g_csvDataInjectThread == nullptr) {
|
||||
if (infoMsg && infoMsgSize > 0) {
|
||||
strncpy(infoMsg, "CSV 注入线程已不存在", infoMsgSize - 1);
|
||||
infoMsg[infoMsgSize - 1] = '\0';
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
g_csvDataInjectThread->stop();
|
||||
g_csvDataInjectThread.reset(); // 释放智能指针
|
||||
return 0;
|
||||
}
|
||||
|
||||
int XNMONITORSERVER_EXPORT XN_StartCollectData(const char *structName, const int structNameLen,
|
||||
|
@ -210,14 +210,23 @@ extern "C"
|
||||
* @param structNameLen 结构体名称长度
|
||||
* @param csvFilePath csv文件路径
|
||||
* @param csvFilePathLen csv文件路径长度
|
||||
* @param injectTimes 注入次数
|
||||
* @param infoMsg 错误信息
|
||||
* @param infoMsgSize 错误信息大小
|
||||
* @return 0: 成功, -1: 失败
|
||||
*/
|
||||
int XNMONITORSERVER_EXPORT XN_InjectDataInterfaceFromCsv(
|
||||
const char *structName, const int structNameLen, const char *csvFilePath,
|
||||
const int csvFilePathLen, int injectTimes, char *infoMsg, int infoMsgSize);
|
||||
int XNMONITORSERVER_EXPORT XN_InjectDataInterfaceFromCsv(const char *structName,
|
||||
const int structNameLen,
|
||||
const char *csvFilePath,
|
||||
const int csvFilePathLen,
|
||||
char *infoMsg, int infoMsgSize);
|
||||
|
||||
/**
|
||||
* @brief 停止csv数据注入
|
||||
* @param infoMsg 错误信息
|
||||
* @param infoMsgSize 错误信息大小
|
||||
* @return 0: 成功, -1: 失败
|
||||
*/
|
||||
int XNMONITORSERVER_EXPORT XN_StopCsvDataInject(char *infoMsg, int infoMsgSize);
|
||||
|
||||
//******************** csv数据采集 *********************
|
||||
|
||||
|
5
XNSimHtml/.gitignore
vendored
Normal file
5
XNSimHtml/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
log/*
|
||||
logs/*
|
||||
upload/*
|
||||
uploads/*
|
||||
node_modules/*
|
BIN
XNSimHtml/assets/icons/png/inject.png
Normal file
BIN
XNSimHtml/assets/icons/png/inject.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
BIN
XNSimHtml/assets/icons/png/inject_b.png
Normal file
BIN
XNSimHtml/assets/icons/png/inject_b.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
@ -23,11 +23,7 @@ class DataMonitor extends HTMLElement {
|
||||
this.handleTreeItemDblClick = this.handleTreeItemDblClick.bind(this);
|
||||
this.handlePlot = this.handlePlot.bind(this);
|
||||
this.handleDelete = this.handleDelete.bind(this);
|
||||
this.handleCsvInject = this.handleCsvInject.bind(this);
|
||||
this.handleModalClose = this.handleModalClose.bind(this);
|
||||
this.handleModalCancel = this.handleModalCancel.bind(this);
|
||||
this.handleModalConfirm = this.handleModalConfirm.bind(this);
|
||||
this.handleFileInput = this.handleFileInput.bind(this);
|
||||
this.handleCsvFileSelect = this.handleCsvFileSelect.bind(this);
|
||||
this.validateCsvFile = this.validateCsvFile.bind(this);
|
||||
|
||||
// 确保 FloatingChartWindow 组件已注册
|
||||
@ -1145,6 +1141,15 @@ class DataMonitor extends HTMLElement {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.csv-file-name {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@ -1313,42 +1318,16 @@ class DataMonitor extends HTMLElement {
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<button class="csv-inject-button" id="csvInjectButton">
|
||||
<img src="assets/icons/png/file.png" alt="CSV" style="width: 16px; height: 16px;">
|
||||
CSV文件注入
|
||||
<input type="file" id="csvFileInput" accept=".csv" style="display: none;" />
|
||||
<span id="csvFileName" class="csv-file-name"></span>
|
||||
<button class="csv-inject-button" id="csvUploadButton">
|
||||
<img src="assets/icons/png/upload.png" alt="上传" style="width: 16px; height: 16px;">
|
||||
上传CSV文件
|
||||
</button>
|
||||
<button class="csv-inject-button" id="csvInjectButton" style="display: none;">
|
||||
<img src="assets/icons/png/inject.png" alt="注入" style="width: 16px; height: 16px;">
|
||||
CSV数据注入
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-overlay" id="csvModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">CSV文件注入</h3>
|
||||
<button class="modal-close" id="modalClose">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>选择CSV文件</label>
|
||||
<div class="file-input-wrapper">
|
||||
<div class="file-input-trigger" id="fileInputTrigger">
|
||||
点击或拖拽文件到此处
|
||||
</div>
|
||||
<input type="file" id="csvFileInput" accept=".csv" />
|
||||
</div>
|
||||
<div class="file-name" id="fileName"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>注入频率 (Hz)</label>
|
||||
<input type="number" id="injectFrequency" min="10" max="10000" step="10" value="100" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>注入时长 (秒)</label>
|
||||
<input type="number" id="injectDuration" min="1" max="3600" step="1" value="60" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="modal-button secondary" id="modalCancel">取消</button>
|
||||
<button class="modal-button primary" id="modalConfirm">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="monitor-container">
|
||||
@ -1791,58 +1770,17 @@ class DataMonitor extends HTMLElement {
|
||||
});
|
||||
|
||||
// 添加CSV文件注入相关的事件监听
|
||||
const csvInjectButton = this.shadowRoot.getElementById('csvInjectButton');
|
||||
const modal = this.shadowRoot.getElementById('csvModal');
|
||||
const modalClose = this.shadowRoot.getElementById('modalClose');
|
||||
const modalCancel = this.shadowRoot.getElementById('modalCancel');
|
||||
const modalConfirm = this.shadowRoot.getElementById('modalConfirm');
|
||||
const csvUploadButton = this.shadowRoot.getElementById('csvUploadButton');
|
||||
const fileInput = this.shadowRoot.getElementById('csvFileInput');
|
||||
const fileInputTrigger = this.shadowRoot.getElementById('fileInputTrigger');
|
||||
|
||||
if (csvInjectButton) {
|
||||
csvInjectButton.addEventListener('click', this.handleCsvInject);
|
||||
}
|
||||
|
||||
if (modalClose) {
|
||||
modalClose.addEventListener('click', this.handleModalClose);
|
||||
}
|
||||
|
||||
if (modalCancel) {
|
||||
modalCancel.addEventListener('click', this.handleModalCancel);
|
||||
}
|
||||
|
||||
if (modalConfirm) {
|
||||
modalConfirm.addEventListener('click', this.handleModalConfirm);
|
||||
if (csvUploadButton && fileInput) {
|
||||
csvUploadButton.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', this.handleFileInput);
|
||||
}
|
||||
|
||||
if (fileInputTrigger) {
|
||||
fileInputTrigger.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
fileInputTrigger.style.borderColor = '#1890ff';
|
||||
});
|
||||
|
||||
fileInputTrigger.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
fileInputTrigger.style.borderColor = '#d9d9d9';
|
||||
});
|
||||
|
||||
fileInputTrigger.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
fileInputTrigger.style.borderColor = '#d9d9d9';
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
fileInput.files = files;
|
||||
this.handleFileInput({ target: fileInput });
|
||||
}
|
||||
});
|
||||
fileInput.addEventListener('change', this.handleCsvFileSelect);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1952,80 +1890,115 @@ class DataMonitor extends HTMLElement {
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 处理CSV文件注入按钮点击事件
|
||||
* @description 处理CSV文件选择事件
|
||||
* @param {Event} event - 文件选择事件
|
||||
*/
|
||||
handleCsvInject() {
|
||||
const modal = this.shadowRoot.getElementById('csvModal');
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 处理模态框关闭事件
|
||||
*/
|
||||
handleModalClose() {
|
||||
const modal = this.shadowRoot.getElementById('csvModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 处理模态框取消按钮点击事件
|
||||
*/
|
||||
handleModalCancel() {
|
||||
this.handleModalClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 处理模态框确认按钮点击事件
|
||||
*/
|
||||
async handleModalConfirm() {
|
||||
const fileInput = this.shadowRoot.getElementById('csvFileInput');
|
||||
const frequencyInput = this.shadowRoot.getElementById('injectFrequency');
|
||||
const durationInput = this.shadowRoot.getElementById('injectDuration');
|
||||
|
||||
if (!fileInput.files.length) {
|
||||
alert('请选择CSV文件');
|
||||
return;
|
||||
}
|
||||
|
||||
const file = fileInput.files[0];
|
||||
const frequency = parseInt(frequencyInput.value);
|
||||
const duration = parseInt(durationInput.value);
|
||||
async handleCsvFileSelect(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
if (!this.validateCsvFile(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: 实现CSV文件注入逻辑
|
||||
console.log('CSV文件注入:', {
|
||||
file: file.name,
|
||||
frequency,
|
||||
duration
|
||||
// 创建 FormData 对象
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// 上传文件
|
||||
const response = await fetch('/api/filesystem/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
this.handleModalClose();
|
||||
} catch (error) {
|
||||
console.error('CSV文件注入失败:', error);
|
||||
alert(`CSV文件注入失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || '文件上传失败');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 处理文件输入事件
|
||||
* @param {Event} event - 文件输入事件
|
||||
*/
|
||||
handleFileInput(event) {
|
||||
const file = event.target.files[0];
|
||||
const fileName = this.shadowRoot.getElementById('fileName');
|
||||
|
||||
if (file) {
|
||||
fileName.textContent = file.name;
|
||||
} else {
|
||||
fileName.textContent = '';
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || '文件上传失败');
|
||||
}
|
||||
|
||||
console.log('CSV文件上传成功:', result.file.name);
|
||||
|
||||
// 验证CSV文件头部
|
||||
const validateResponse = await fetch(`/api/filesystem/validate-csv-headers?filename=${encodeURIComponent(result.file.name)}`);
|
||||
if (!validateResponse.ok) {
|
||||
throw new Error('验证CSV文件头部失败');
|
||||
}
|
||||
|
||||
const validateResult = await validateResponse.json();
|
||||
if (!validateResult.success) {
|
||||
throw new Error(validateResult.message || '验证CSV文件头部失败');
|
||||
}
|
||||
|
||||
// 检查每个头部是否在接口表中
|
||||
const missingInterfaces = [];
|
||||
const invalidInterfaces = [];
|
||||
|
||||
// 检查第一个接口(时间)是否在接口表中
|
||||
const firstHeader = validateResult.headers[0];
|
||||
const firstHeaderExists = this.interfaces.some(interfaceItem =>
|
||||
interfaceItem.InterfaceName === firstHeader
|
||||
);
|
||||
if (firstHeaderExists) {
|
||||
invalidInterfaces.push(`第一个接口 "${firstHeader}" 不应该在接口表中`);
|
||||
}
|
||||
|
||||
// 检查其他接口是否在接口表中
|
||||
for (let i = 1; i < validateResult.headers.length; i++) {
|
||||
const header = validateResult.headers[i];
|
||||
const exists = this.interfaces.some(interfaceItem =>
|
||||
interfaceItem.InterfaceName === header
|
||||
);
|
||||
if (!exists) {
|
||||
missingInterfaces.push(header);
|
||||
}
|
||||
}
|
||||
|
||||
// 合并错误信息
|
||||
const errorMessages = [];
|
||||
if (invalidInterfaces.length > 0) {
|
||||
errorMessages.push(invalidInterfaces.join('\n'));
|
||||
}
|
||||
if (missingInterfaces.length > 0) {
|
||||
errorMessages.push(`以下接口在系统中不存在:\n${missingInterfaces.join('\n')}`);
|
||||
}
|
||||
|
||||
if (errorMessages.length > 0) {
|
||||
// 删除已上传的文件
|
||||
try {
|
||||
const deleteResponse = await fetch(`/api/filesystem/upload/${encodeURIComponent(result.file.name)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (!deleteResponse.ok) {
|
||||
console.error('删除验证失败的文件时出错');
|
||||
}
|
||||
} catch (deleteError) {
|
||||
console.error('删除验证失败的文件时出错:', deleteError);
|
||||
}
|
||||
throw new Error(errorMessages.join('\n\n'));
|
||||
}
|
||||
|
||||
console.log('CSV文件头部验证通过');
|
||||
|
||||
// 更新文件名显示
|
||||
const csvFileName = this.shadowRoot.getElementById('csvFileName');
|
||||
if (csvFileName) {
|
||||
csvFileName.textContent = result.file.name;
|
||||
}
|
||||
|
||||
// 显示数据注入按钮
|
||||
const csvInjectButton = this.shadowRoot.getElementById('csvInjectButton');
|
||||
if (csvInjectButton) {
|
||||
csvInjectButton.style.display = 'flex';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('CSV文件处理失败:', error);
|
||||
alert(`CSV文件处理失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2035,7 +2008,19 @@ class DataMonitor extends HTMLElement {
|
||||
* @returns {boolean} 是否验证通过
|
||||
*/
|
||||
validateCsvFile(file) {
|
||||
// TODO: 实现CSV文件验证逻辑
|
||||
// 检查文件类型
|
||||
if (!file.name.toLowerCase().endsWith('.csv')) {
|
||||
alert('请选择CSV文件');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查文件大小(限制为10MB)
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
if (file.size > maxSize) {
|
||||
alert('文件大小不能超过10MB');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -2052,6 +2037,6 @@ class DataMonitor extends HTMLElement {
|
||||
this.stopDataUpdateTimer();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('data-monitor', DataMonitor);
|
||||
|
||||
|
||||
|
@ -281,4 +281,90 @@ router.post('/stop-continuous', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @brief 停止所有持续注入数据
|
||||
* @route POST /api/data-monitor/stop-all-continuous
|
||||
* @returns {Object} 返回停止结果
|
||||
*/
|
||||
router.post('/stop-all-continuous', async (req, res) => {
|
||||
try {
|
||||
// 获取所有正在注入的结构体
|
||||
const injectingStructs = Array.from(continuousInjectStatus.keys());
|
||||
|
||||
if (injectingStructs.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: '当前没有正在进行的注入'
|
||||
});
|
||||
}
|
||||
|
||||
// 停止所有结构体的注入
|
||||
const results = [];
|
||||
for (const structName of injectingStructs) {
|
||||
const result = systemMonitor.stopInjectContinuous(structName);
|
||||
if (result.includes('失败')) {
|
||||
results.push({ structName, success: false, message: result });
|
||||
} else {
|
||||
results.push({ structName, success: true, message: '停止成功' });
|
||||
}
|
||||
continuousInjectStatus.delete(structName);
|
||||
}
|
||||
|
||||
// 检查是否所有结构体都成功停止
|
||||
const allSuccess = results.every(r => r.success);
|
||||
|
||||
res.json({
|
||||
success: allSuccess,
|
||||
message: allSuccess ? '所有持续注入已停止' : '部分持续注入停止失败',
|
||||
details: results
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: `停止所有持续注入数据失败: ${error.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @brief 从CSV文件注入数据
|
||||
* @route POST /api/data-monitor/inject-csv
|
||||
* @param {string} structName - 结构体名称
|
||||
* @param {string} csvFilePath - CSV文件路径
|
||||
* @returns {Object} 返回注入结果
|
||||
*/
|
||||
router.post('/inject-csv', async (req, res) => {
|
||||
try {
|
||||
const { structName, csvFilePath} = req.body;
|
||||
if (!structName || !csvFilePath) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '结构体名称、CSV文件路径和注入次数不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
const result = systemMonitor.injectDataInterfaceFromCsv(structName, csvFilePath);
|
||||
if (result.includes('失败')) {
|
||||
return res.status(500).json({ success: false, message: result });
|
||||
}
|
||||
res.json({ success: true, message: result });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: `从CSV文件注入数据失败: ${error.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @brief 停止CSV数据注入
|
||||
* @route POST /api/data-monitor/stop-csv-inject
|
||||
* @returns {Object} 返回停止结果
|
||||
*/
|
||||
router.post('/stop-csv-inject', async (req, res) => {
|
||||
try {
|
||||
const result = systemMonitor.stopCsvDataInject();
|
||||
if (result.includes('失败')) {
|
||||
return res.status(500).json({ success: false, message: result });
|
||||
}
|
||||
res.json({ success: true, message: result });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: `停止CSV数据注入失败: ${error.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
@ -1,8 +1,47 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const fs = require('fs').promises;
|
||||
const fsPromises = require('fs').promises;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getActualLogPath } = require('../utils/file-utils');
|
||||
const multer = require('multer');
|
||||
const { getActualLogPath, getUploadPath, saveUploadedFile, isAllowedFileType } = require('../utils/file-utils');
|
||||
|
||||
// 获取上传目录路径
|
||||
const uploadPath = getUploadPath();
|
||||
|
||||
// 配置 multer 存储
|
||||
const storage = multer.diskStorage({
|
||||
destination: async function (req, file, cb) {
|
||||
try {
|
||||
const uploadPath = await getUploadPath();
|
||||
cb(null, uploadPath);
|
||||
} catch (error) {
|
||||
cb(error);
|
||||
}
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
cb(null, file.originalname);
|
||||
}
|
||||
});
|
||||
|
||||
// 文件过滤器
|
||||
const fileFilter = (req, file, cb) => {
|
||||
// 允许的文件类型
|
||||
const allowedTypes = ['.csv'];
|
||||
if (isAllowedFileType(file.originalname, allowedTypes)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('不支持的文件类型'));
|
||||
}
|
||||
};
|
||||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
fileFilter: fileFilter,
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024 // 限制文件大小为10MB
|
||||
}
|
||||
});
|
||||
|
||||
// 读取目录内容
|
||||
router.get('/readdir', async (req, res) => {
|
||||
@ -17,7 +56,7 @@ router.get('/readdir', async (req, res) => {
|
||||
|
||||
// 检查目录是否存在
|
||||
try {
|
||||
const stats = await fs.stat(logDirPath);
|
||||
const stats = await fsPromises.stat(logDirPath);
|
||||
if (!stats.isDirectory()) {
|
||||
return res.status(400).json({ error: '指定的路径不是目录' });
|
||||
}
|
||||
@ -25,7 +64,7 @@ router.get('/readdir', async (req, res) => {
|
||||
// 如果目录不存在,尝试创建它
|
||||
if (statError.code === 'ENOENT') {
|
||||
try {
|
||||
await fs.mkdir(logDirPath, { recursive: true });
|
||||
await fsPromises.mkdir(logDirPath, { recursive: true });
|
||||
} catch (mkdirError) {
|
||||
console.error('创建日志目录失败:', mkdirError);
|
||||
return res.status(500).json({ error: '创建日志目录失败' });
|
||||
@ -36,7 +75,7 @@ router.get('/readdir', async (req, res) => {
|
||||
}
|
||||
|
||||
// 读取目录内容
|
||||
const files = await fs.readdir(logDirPath);
|
||||
const files = await fsPromises.readdir(logDirPath);
|
||||
|
||||
// 返回文件列表
|
||||
res.json({ files });
|
||||
@ -78,7 +117,7 @@ router.get('/stat', async (req, res) => {
|
||||
}
|
||||
|
||||
// 获取文件状态
|
||||
const stats = await fs.stat(filePath);
|
||||
const stats = await fsPromises.stat(filePath);
|
||||
|
||||
// 返回文件信息
|
||||
res.json({
|
||||
@ -133,7 +172,7 @@ router.get('/readFile', async (req, res) => {
|
||||
}
|
||||
|
||||
// 获取文件状态
|
||||
const stats = await fs.stat(filePath);
|
||||
const stats = await fsPromises.stat(filePath);
|
||||
|
||||
// 检查文件大小,限制读取过大的文件
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
@ -142,7 +181,7 @@ router.get('/readFile', async (req, res) => {
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const content = await fsPromises.readFile(filePath, 'utf-8');
|
||||
|
||||
// 设置响应头
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
@ -164,4 +203,159 @@ router.get('/readFile', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 获取上传文件列表
|
||||
router.get('/upload-files', async (req, res) => {
|
||||
try {
|
||||
const uploadPath = await getUploadPath();
|
||||
|
||||
// 读取目录内容
|
||||
const files = await fsPromises.readdir(uploadPath);
|
||||
|
||||
// 获取每个文件的详细信息
|
||||
const fileDetails = await Promise.all(files.map(async (fileName) => {
|
||||
const filePath = path.join(uploadPath, fileName);
|
||||
const stats = await fsPromises.stat(filePath);
|
||||
return {
|
||||
name: fileName,
|
||||
size: stats.size,
|
||||
created: stats.birthtime,
|
||||
modified: stats.mtime,
|
||||
path: filePath
|
||||
};
|
||||
}));
|
||||
|
||||
res.json({ files: fileDetails });
|
||||
} catch (error) {
|
||||
console.error('获取上传文件列表失败:', error);
|
||||
res.status(500).json({ error: '获取上传文件列表失败', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 上传文件
|
||||
router.post('/upload', upload.single('file'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: '未提供文件' });
|
||||
}
|
||||
|
||||
// 获取上传目录路径
|
||||
const uploadPath = await getUploadPath();
|
||||
|
||||
// 确保上传目录存在
|
||||
try {
|
||||
await fsPromises.access(uploadPath);
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
// 如果目录不存在,创建它
|
||||
await fsPromises.mkdir(uploadPath, { recursive: true });
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存文件并获取最终路径
|
||||
const finalPath = await saveUploadedFile(req.file);
|
||||
|
||||
// 获取文件状态
|
||||
const stats = await fsPromises.stat(finalPath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
file: {
|
||||
name: path.basename(finalPath),
|
||||
size: stats.size,
|
||||
path: finalPath,
|
||||
created: stats.birthtime,
|
||||
modified: stats.mtime
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('文件上传失败:', error);
|
||||
res.status(500).json({ error: '文件上传失败', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除上传的文件
|
||||
router.delete('/upload/:filename', async (req, res) => {
|
||||
try {
|
||||
const { filename } = req.params;
|
||||
const uploadPath = await getUploadPath();
|
||||
const filePath = path.join(uploadPath, filename);
|
||||
|
||||
// 安全检查:确保文件在上传目录内
|
||||
if (!filePath.startsWith(uploadPath)) {
|
||||
return res.status(403).json({ error: '无权删除该文件' });
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
await fsPromises.unlink(filePath);
|
||||
|
||||
res.json({ success: true, message: '文件删除成功' });
|
||||
} catch (error) {
|
||||
console.error('删除文件失败:', error);
|
||||
|
||||
if (error.code === 'ENOENT') {
|
||||
return res.status(404).json({ error: '文件不存在' });
|
||||
}
|
||||
|
||||
res.status(500).json({ error: '删除文件失败', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 验证CSV文件头部
|
||||
router.get('/validate-csv-headers', async (req, res) => {
|
||||
try {
|
||||
const { filename } = req.query;
|
||||
if (!filename) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '未提供文件名'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取上传目录路径
|
||||
const uploadPath = await getUploadPath();
|
||||
const filePath = path.join(uploadPath, filename);
|
||||
|
||||
// 检查文件是否存在
|
||||
try {
|
||||
await fsPromises.access(filePath);
|
||||
} catch (error) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '文件不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 只读取文件的第一行
|
||||
const fileStream = fs.createReadStream(filePath, { encoding: 'utf8' });
|
||||
let firstLine = '';
|
||||
|
||||
try {
|
||||
for await (const chunk of fileStream) {
|
||||
const lines = chunk.split('\n');
|
||||
firstLine = lines[0];
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
// 确保关闭文件流
|
||||
fileStream.destroy();
|
||||
}
|
||||
|
||||
// 解析CSV头部
|
||||
const headers = firstLine.split(',').map(header => header.trim());
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
headers: headers
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('验证CSV文件头部失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '验证CSV文件头部失败: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
@ -135,24 +135,9 @@ async function getUploadPath() {
|
||||
|
||||
// 保存上传的文件
|
||||
async function saveUploadedFile(file) {
|
||||
try {
|
||||
const uploadPath = await getUploadPath();
|
||||
const filePath = path.join(uploadPath, file.originalname);
|
||||
|
||||
// 如果文件已存在,添加时间戳
|
||||
if (fs.existsSync(filePath)) {
|
||||
const timestamp = new Date().getTime();
|
||||
const ext = path.extname(file.originalname);
|
||||
const name = path.basename(file.originalname, ext);
|
||||
const newFileName = `${name}_${timestamp}${ext}`;
|
||||
return path.join(uploadPath, newFileName);
|
||||
}
|
||||
|
||||
return filePath;
|
||||
} catch (error) {
|
||||
console.error('处理上传文件路径失败:', error);
|
||||
throw error;
|
||||
}
|
||||
const uploadPath = await getUploadPath();
|
||||
const filePath = path.join(uploadPath, file.originalname);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
// 检查文件类型是否允许
|
||||
|
@ -59,7 +59,9 @@ try {
|
||||
'XN_GetDataMonitorInfo': ['int', [StringType, 'int', StringType, 'int', StringType, 'int', StringType, 'int']],
|
||||
'XN_InjectDataInterface': ['int', [StringType, 'int', StringType, 'int', StringType, 'int']],
|
||||
'XN_StartInjectContinuous': ['int', [StringType, 'int', StringType, 'int', 'double', StringType, 'int']],
|
||||
'XN_StopInjectContinuous': ['int', [StringType, 'int', StringType, 'int']]
|
||||
'XN_StopInjectContinuous': ['int', [StringType, 'int', StringType, 'int']],
|
||||
'XN_InjectDataInterfaceFromCsv': ['int', [StringType, 'int', StringType, 'int', StringType, 'int']],
|
||||
'XN_StopCsvDataInject': ['int', [StringType, 'int']]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`加载 ${monitorLibName} 失败:`, error);
|
||||
@ -437,6 +439,49 @@ function stopInjectContinuous(structName) {
|
||||
}
|
||||
}
|
||||
|
||||
// 从CSV文件注入数据
|
||||
function injectDataInterfaceFromCsv(structName, csvFilePath) {
|
||||
if (!monitorLib) {
|
||||
return '监控服务器库未加载';
|
||||
}
|
||||
try {
|
||||
const errorMsg = Buffer.alloc(1024);
|
||||
const result = monitorLib.XN_InjectDataInterfaceFromCsv(
|
||||
structName,
|
||||
structName.length,
|
||||
csvFilePath,
|
||||
csvFilePath.length,
|
||||
errorMsg,
|
||||
errorMsg.length
|
||||
);
|
||||
|
||||
if (result !== 0) {
|
||||
return `注入失败: ${errorMsg.toString('utf8').replace(/\0/g, '')}`;
|
||||
}
|
||||
return '注入成功';
|
||||
} catch (error) {
|
||||
return `注入失败: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 停止CSV数据注入
|
||||
function stopCsvDataInject() {
|
||||
if (!monitorLib) {
|
||||
return '监控服务器库未加载';
|
||||
}
|
||||
try {
|
||||
const errorMsg = Buffer.alloc(1024);
|
||||
const result = monitorLib.XN_StopCsvDataInject(errorMsg, errorMsg.length);
|
||||
|
||||
if (result !== 0) {
|
||||
return `停止失败: ${errorMsg.toString('utf8').replace(/\0/g, '')}`;
|
||||
}
|
||||
return '停止成功';
|
||||
} catch (error) {
|
||||
return `停止失败: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loginLib,
|
||||
monitorLib,
|
||||
@ -460,5 +505,7 @@ module.exports = {
|
||||
getDataMonitorInfo,
|
||||
injectDataInterface,
|
||||
startInjectContinuous,
|
||||
stopInjectContinuous
|
||||
stopInjectContinuous,
|
||||
injectDataInterfaceFromCsv,
|
||||
stopCsvDataInject
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user