Browse Source

feat: 添加文件操作相关处理,支持确保目录存在、追加文件内容、检查文件存在性及写入CSV文件头

master
cles 1 week ago
parent
commit
66bdc1df5c
  1. 1
      README.md
  2. 69
      src/main/ipcRouter.js
  3. 16
      src/renderer/index.html
  4. 2
      src/renderer/src/assets/base.css
  5. 4
      src/renderer/src/components/MeasurementPointSetting/MeasurementPointSetting.module.css
  6. 160
      src/renderer/src/components/SystemSettings/SystemSettings.jsx

1
README.md

@ -338,4 +338,3 @@ chore: 构建或工具相关
3. 提交更改 (`git commit -m 'feat: add amazing feature'`) 3. 提交更改 (`git commit -m 'feat: add amazing feature'`)
4. 推送到分支 (`git push origin feature/amazing-feature`) 4. 推送到分支 (`git push origin feature/amazing-feature`)
5. 创建 Pull Request 5. 创建 Pull Request

69
src/main/ipcRouter.js

@ -2,6 +2,8 @@ import { ipcMain } from 'electron'
import { dialog, shell } from 'electron' import { dialog, shell } from 'electron'
import dgram from 'dgram' import dgram from 'dgram'
import net from 'net' import net from 'net'
import { appendFileSync, mkdirSync, existsSync } from 'fs'
import { dirname } from 'path'
import { IPC_EVENT } from '../renderer/src/common/ipcEvents.js' import { IPC_EVENT } from '../renderer/src/common/ipcEvents.js'
import log from 'electron-log' import log from 'electron-log'
import ReconnectManager from './reconnectManager.js' import ReconnectManager from './reconnectManager.js'
@ -191,6 +193,11 @@ export function registerIpRouter() {
// 存储目录相关处理 // 存储目录相关处理
ipcMain.handle('open-directory', openDirectory) ipcMain.handle('open-directory', openDirectory)
ipcMain.handle('select-directory', selectDirectory) 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) => { const searchDevice = (event) => {
@ -1483,3 +1490,65 @@ const selectDirectory = async (event, options) => {
return { success: false, error: error.message } 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 }
}
}

16
src/renderer/index.html

@ -1,16 +1,16 @@
<!doctype html> <!doctype html>
<html> <html>
<head>
<head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>FlexometerSetup is developed by FS</title> <title>FlexometerSetup is developed by FS</title>
<meta http-equiv="Content-Security-Policy" <meta
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:" /> http-equiv="Content-Security-Policy"
</head> content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/>
</head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.jsx"></script> <script type="module" src="/src/main.jsx"></script>
</body> </body>
</html> </html>

2
src/renderer/src/assets/base.css

@ -1,4 +1,4 @@
:root{ :root {
--bg-color: #ebe9e9; --bg-color: #ebe9e9;
--border-color: #c4c2c2; --border-color: #c4c2c2;
} }

4
src/renderer/src/components/MeasurementPointSetting/MeasurementPointSetting.module.css

@ -39,12 +39,10 @@
.label { .label {
text-align: right; text-align: right;
margin-right: 8px; margin-right: 8px;
} }
.input { .input {
height: 24px; height: 24px;
} }
.select { .select {
@ -66,8 +64,6 @@
flex-direction: column; flex-direction: column;
} }
.actionButtons { .actionButtons {
margin-top: 8px; margin-top: 8px;
gap: 8px; gap: 8px;

160
src/renderer/src/components/SystemSettings/SystemSettings.jsx

@ -8,7 +8,7 @@ import {
EyeOutlined, EyeOutlined,
EditOutlined EditOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import { useState, useEffect } from 'react' import { useState, useEffect, useCallback, useRef } from 'react'
import { IPC_EVENT } from '../../common/ipcEvents' import { IPC_EVENT } from '../../common/ipcEvents'
import useDeviceStore from '../../stores/deviceStore' import useDeviceStore from '../../stores/deviceStore'
@ -22,6 +22,10 @@ function SystemSettings() {
const [clearZeroLoading, setClearZeroLoading] = useState(false) const [clearZeroLoading, setClearZeroLoading] = useState(false)
const [storagePath, setStoragePath] = useState('') const [storagePath, setStoragePath] = useState('')
//
const [realtimeDataEnabled, setRealtimeDataEnabled] = useState(false)
const [alarmDataEnabled, setAlarmDataEnabled] = useState(false)
// //
const connectedDevice = useDeviceStore((state) => state.connectedDevice) const connectedDevice = useDeviceStore((state) => state.connectedDevice)
const reconnectEnabled = useDeviceStore((state) => state.reconnectEnabled) const reconnectEnabled = useDeviceStore((state) => state.reconnectEnabled)
@ -35,6 +39,24 @@ function SystemSettings() {
const setAlarmEnabled = useDeviceStore((state) => state.setAlarmEnabled) const setAlarmEnabled = useDeviceStore((state) => state.setAlarmEnabled)
const setAlarmLimits = useDeviceStore((state) => state.setAlarmLimits) 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(() => { useEffect(() => {
const reconnectStatusHandler = (event, result) => { 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 () => { const handleOpenStoragePath = async () => {
if (!storagePath) { 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 () => { const handleReadParam = async () => {
if (!selectedParam) { if (!selectedParam) {
@ -522,10 +665,21 @@ function SystemSettings() {
placeholder="存储目录路径" placeholder="存储目录路径"
/> />
</Flex> </Flex>
<Checkbox className={styles.checkboxRight} disabled={!connectedDevice}> <Checkbox
className={styles.checkboxRight}
disabled={!connectedDevice}
checked={realtimeDataEnabled}
onChange={(e) => setRealtimeDataEnabled(e.target.checked)}
>
实时数据 实时数据
</Checkbox> </Checkbox>
<Checkbox disabled={!connectedDevice}>报警数据</Checkbox> <Checkbox
disabled={!connectedDevice}
checked={alarmDataEnabled}
onChange={(e) => setAlarmDataEnabled(e.target.checked)}
>
报警数据
</Checkbox>
</div> </div>
</Flex> </Flex>
) )

Loading…
Cancel
Save