diff --git a/README.md b/README.md index 61da68a..4dd1ccb 100644 --- a/README.md +++ b/README.md @@ -338,4 +338,3 @@ chore: 构建或工具相关 3. 提交更改 (`git commit -m 'feat: add amazing feature'`) 4. 推送到分支 (`git push origin feature/amazing-feature`) 5. 创建 Pull Request - diff --git a/src/main/ipcRouter.js b/src/main/ipcRouter.js index 12d13e5..06dc80a 100644 --- a/src/main/ipcRouter.js +++ b/src/main/ipcRouter.js @@ -2,6 +2,8 @@ import { ipcMain } from 'electron' import { dialog, shell } from 'electron' import dgram from 'dgram' import net from 'net' +import { appendFileSync, mkdirSync, existsSync } from 'fs' +import { dirname } from 'path' import { IPC_EVENT } from '../renderer/src/common/ipcEvents.js' import log from 'electron-log' import ReconnectManager from './reconnectManager.js' @@ -191,6 +193,11 @@ export function registerIpRouter() { // 存储目录相关处理 ipcMain.handle('open-directory', openDirectory) ipcMain.handle('select-directory', selectDirectory) + // 文件操作相关处理 + ipcMain.handle('ensure-directory', ensureDirectory) + ipcMain.handle('append-to-file', appendToFile) + ipcMain.handle('check-file-exists', checkFileExists) + ipcMain.handle('write-csv-header', writeCSVHeader) } // 搜索设备 const searchDevice = (event) => { @@ -1483,3 +1490,65 @@ const selectDirectory = async (event, options) => { return { success: false, error: error.message } } } + +// 确保目录存在 +const ensureDirectory = async (event, dirPath) => { + try { + if (!existsSync(dirPath)) { + mkdirSync(dirPath, { recursive: true }) + log.info(`Created directory: ${dirPath}`) + } + return { success: true } + } catch (error) { + log.error('Failed to create directory:', error) + return { success: false, error: error.message } + } +} + +// 追加内容到文件 +const appendToFile = async (event, filePath, content) => { + try { + // 确保父目录存在 + const dir = dirname(filePath) + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }) + } + + appendFileSync(filePath, content, 'utf8') + return { success: true } + } catch (error) { + log.error('Failed to append to file:', error) + return { success: false, error: error.message } + } +} + +// 写入CSV文件头(包含BOM) +const writeCSVHeader = async (event, filePath, content) => { + try { + // 确保父目录存在 + const dir = dirname(filePath) + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }) + } + + // 添加UTF-8 BOM以确保Excel正确显示中文 + const bom = '\uFEFF' + appendFileSync(filePath, bom + content, 'utf8') + log.info(`Created CSV file with header: ${filePath}`) + return { success: true } + } catch (error) { + log.error('Failed to write CSV header:', error) + return { success: false, error: error.message } + } +} + +// 检查文件是否存在 +const checkFileExists = async (event, filePath) => { + try { + const exists = existsSync(filePath) + return { success: true, exists } + } catch (error) { + log.error('Failed to check file existence:', error) + return { success: false, error: error.message } + } +} diff --git a/src/renderer/index.html b/src/renderer/index.html index 76fb425..511a3b8 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -1,16 +1,16 @@ + + + FlexometerSetup is developed by FS + + - - - FlexometerSetup is developed by FS - - - - -
- - - + +
+ + diff --git a/src/renderer/src/assets/base.css b/src/renderer/src/assets/base.css index bd21df9..0529393 100644 --- a/src/renderer/src/assets/base.css +++ b/src/renderer/src/assets/base.css @@ -1,4 +1,4 @@ -:root{ +:root { --bg-color: #ebe9e9; --border-color: #c4c2c2; } diff --git a/src/renderer/src/components/MeasurementPointSetting/MeasurementPointSetting.module.css b/src/renderer/src/components/MeasurementPointSetting/MeasurementPointSetting.module.css index 8593129..912527a 100644 --- a/src/renderer/src/components/MeasurementPointSetting/MeasurementPointSetting.module.css +++ b/src/renderer/src/components/MeasurementPointSetting/MeasurementPointSetting.module.css @@ -39,12 +39,10 @@ .label { text-align: right; margin-right: 8px; - } .input { height: 24px; - } .select { @@ -66,8 +64,6 @@ flex-direction: column; } - - .actionButtons { margin-top: 8px; gap: 8px; diff --git a/src/renderer/src/components/SystemSettings/SystemSettings.jsx b/src/renderer/src/components/SystemSettings/SystemSettings.jsx index 7be0edb..cb4a968 100644 --- a/src/renderer/src/components/SystemSettings/SystemSettings.jsx +++ b/src/renderer/src/components/SystemSettings/SystemSettings.jsx @@ -8,7 +8,7 @@ import { EyeOutlined, EditOutlined } from '@ant-design/icons' -import { useState, useEffect } from 'react' +import { useState, useEffect, useCallback, useRef } from 'react' import { IPC_EVENT } from '../../common/ipcEvents' import useDeviceStore from '../../stores/deviceStore' @@ -22,6 +22,10 @@ function SystemSettings() { const [clearZeroLoading, setClearZeroLoading] = useState(false) const [storagePath, setStoragePath] = useState('') + // 新增:实时数据记录状态 + const [realtimeDataEnabled, setRealtimeDataEnabled] = useState(false) + const [alarmDataEnabled, setAlarmDataEnabled] = useState(false) + // 获取设备连接状态和重连配置 const connectedDevice = useDeviceStore((state) => state.connectedDevice) const reconnectEnabled = useDeviceStore((state) => state.reconnectEnabled) @@ -35,6 +39,24 @@ function SystemSettings() { const setAlarmEnabled = useDeviceStore((state) => state.setAlarmEnabled) const setAlarmLimits = useDeviceStore((state) => state.setAlarmLimits) + // 使用ref来存储最新的状态值,避免闭包问题 + const realtimeDataEnabledRef = useRef(realtimeDataEnabled) + const connectedDeviceRef = useRef(connectedDevice) + const storagePathRef = useRef(storagePath) + + // 更新ref值 + useEffect(() => { + realtimeDataEnabledRef.current = realtimeDataEnabled + }, [realtimeDataEnabled]) + + useEffect(() => { + connectedDeviceRef.current = connectedDevice + }, [connectedDevice]) + + useEffect(() => { + storagePathRef.current = storagePath + }, [storagePath]) + // 监听重连状态更新 useEffect(() => { const reconnectStatusHandler = (event, result) => { @@ -78,6 +100,87 @@ function SystemSettings() { } }, []) + // 新增:写入实时数据到CSV文件 + const writeRealtimeDataToCSV = useCallback( + async (data) => { + if ( + !realtimeDataEnabledRef.current || + !connectedDeviceRef.current?.ip || + !storagePathRef.current + ) { + return + } + + try { + // 构建文件路径: 存储路径/IP地址/实时数据/YYYY-MM-DD.csv + const today = new Date() + const dateStr = + today.getFullYear() + + String(today.getMonth() + 1).padStart(2, '0') + + String(today.getDate()).padStart(2, '0') + + const ipFolder = connectedDeviceRef.current.ip // 直接使用IP地址作为文件夹名 + const realtimeDataDir = `${storagePathRef.current}/${ipFolder}/实时数据` + const csvFilePath = `${realtimeDataDir}/${dateStr}.csv` + + // 确保目录存在 + const dirResult = await window.electron.ipcRenderer.invoke( + 'ensure-directory', + realtimeDataDir + ) + if (!dirResult.success) { + console.error('创建实时数据目录失败:', dirResult.error) + return + } + + // 检查CSV文件是否存在,如果不存在则先写入表头 + const fileExistsResult = await window.electron.ipcRenderer.invoke( + 'check-file-exists', + csvFilePath + ) + if (fileExistsResult.success && !fileExistsResult.exists) { + const sensorCount = data.values.sensors ? data.values.sensors.length : 0 + const header = createCSVHeader(sensorCount) + await window.electron.ipcRenderer.invoke('write-csv-header', csvFilePath, header) + } + + // 追加数据行 + const dataRow = sensorDataToCSVRow(data) + const appendResult = await window.electron.ipcRenderer.invoke( + 'append-to-file', + csvFilePath, + dataRow + ) + + if (!appendResult.success) { + console.error('写入CSV文件失败:', appendResult.error) + } + } catch (error) { + console.error('写入实时数据失败:', error) + } + }, + [] // 空依赖数组,因为我们使用ref来访问最新的值 + ) + + // 监听实时数据并记录到CSV + useEffect(() => { + if (window?.electron?.ipcRenderer?.on && IPC_EVENT?.RESULT_REPLY) { + const handler = (event, data) => { + console.log('收到主进程RESULT_REPLY:', data) + + // 检查数据格式 + if (data && data.values && data.values.sensors && Array.isArray(data.values.sensors)) { + // 直接调用写入函数,内部会检查是否启用实时数据记录 + writeRealtimeDataToCSV(data) + } + } + window.electron.ipcRenderer.on(IPC_EVENT.RESULT_REPLY, handler) + return () => { + window.electron.ipcRenderer.removeListener(IPC_EVENT.RESULT_REPLY, handler) + } + } + }, [writeRealtimeDataToCSV]) + // 打开存储目录 const handleOpenStoragePath = async () => { if (!storagePath) { @@ -119,6 +222,46 @@ function SystemSettings() { } } + //创建CSV表头(动态根据测点数量) + const createCSVHeader = (sensorCount = 0) => { + const headers = ['数据记录时间'] + for (let i = 1; i <= sensorCount; i++) { + headers.push( + `测点 ${i} 基准标靶`, + `测点 ${i} 计算系数`, + `测点 ${i}xReal 坐标`, + `测点 ${i}yReal 坐标` + ) + } + return headers.join(',') + '\n' + } + + // 将传感器数据转换为CSV行(动态处理所有测点) + const sensorDataToCSVRow = (data) => { + const timestamp = new Date().toLocaleString() + const sensors = data.values.sensors || [] + + const row = [timestamp] + + // 动态处理所有测点的数据 + for (let i = 0; i < sensors.length; i++) { + const sensor = sensors[i] + if (sensor) { + row.push( + sensor.tar || '', + sensor.arg || '', + sensor.xReal !== null && sensor.xReal !== undefined ? sensor.xReal : '无数据', + sensor.yReal !== null && sensor.yReal !== undefined ? sensor.yReal : '无数据' + ) + } else { + // 如果没有对应测点数据,填入空值 + row.push('', '', '无数据', '无数据') + } + } + + return row.join(',') + '\n' + } + // 读取参数函数 const handleReadParam = async () => { if (!selectedParam) { @@ -522,10 +665,21 @@ function SystemSettings() { placeholder="存储目录路径" /> - + setRealtimeDataEnabled(e.target.checked)} + > 实时数据 - 报警数据 + setAlarmDataEnabled(e.target.checked)} + > + 报警数据 + )