Compare commits

...

2 Commits

  1. 1
      README.md
  2. 106
      src/main/ipcRouter.js
  3. 25
      src/preload/index.js
  4. 24
      src/renderer/index.html
  5. 2
      src/renderer/src/assets/base.css
  6. 4
      src/renderer/src/components/MeasurementPointSetting/MeasurementPointSetting.module.css
  7. 236
      src/renderer/src/components/SystemSettings/SystemSettings.jsx

1
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

106
src/main/ipcRouter.js

@ -1,6 +1,9 @@
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'
@ -187,6 +190,14 @@ export function registerIpRouter() {
ipcMain.handle(IPC_EVENT.INVALID_DATA_COUNT_SET, invalidDataCountSet)
ipcMain.handle(IPC_EVENT.IMAGE_SEND_ENABLED, imageSendEnabledSet)
ipcMain.handle(IPC_EVENT.IS_CLEAR_ZERO, isClearZeroSet)
// 存储目录相关处理
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) => {
@ -1446,3 +1457,98 @@ const stopHeartbeatCheck = (ip) => {
lastHeartbeatTime.delete(ip)
log.info(`Stopped heartbeat check for ${ip}`)
}
// 存储目录相关处理函数
const openDirectory = async (event, path) => {
try {
await shell.openPath(path)
log.info(`Opened directory: ${path}`)
return { success: true }
} catch (error) {
log.error('Failed to open directory:', error)
return { success: false, error: error.message }
}
}
const selectDirectory = async (event, options) => {
try {
const result = await dialog.showOpenDialog({
title: options.title || '选择目录',
properties: ['openDirectory', 'createDirectory'],
defaultPath: options.defaultPath
})
if (result.canceled) {
return { success: false, canceled: true }
}
const selectedPath = result.filePaths[0]
log.info(`Directory selected: ${selectedPath}`)
return { success: true, path: selectedPath }
} catch (error) {
log.error('Failed to select directory:', error)
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 }
}
}

25
src/preload/index.js

@ -1,7 +1,30 @@
import { contextBridge } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
import { join } from 'path'
import { homedir } from 'os'
import { existsSync, mkdirSync } from 'fs'
import log from 'electron-log'
const api = {}
const api = {
// 获取用户主目录
getHomeDir: () => homedir(),
// 获取默认存储目录
getDefaultStoragePath: () => {
const defaultPath = join(homedir(), 'FlexometerSetup', 'data')
// 检查目录是否存在,不存在则创建
try {
if (!existsSync(defaultPath)) {
mkdirSync(defaultPath, { recursive: true })
log.info('Created default storage directory at:', defaultPath)
}
} catch (error) {
log.error('Error creating default storage directory:', error)
return homedir() // 出错时返回用户主目录
}
return defaultPath
}
}
if (process.contextIsolated) {
try {

24
src/renderer/index.html

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

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

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

4
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;

236
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'
@ -20,6 +20,11 @@ function SystemSettings() {
const [settingLoading, setSettingLoading] = useState(false)
const [imageControlLoading, setImageControlLoading] = useState(false)
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)
@ -34,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) => {
@ -64,6 +87,181 @@ function SystemSettings() {
}
}, [setReconnecting])
//
useEffect(() => {
const savedPath = localStorage.getItem('storagePath')
if (savedPath) {
setStoragePath(savedPath)
} else {
// 使 API
const defaultPath = window.api.getDefaultStoragePath()
setStoragePath(defaultPath)
localStorage.setItem('storagePath', defaultPath)
}
}, [])
// 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) {
message.warning('存储路径未设置')
return
}
try {
// 使 IPC
const result = await window.electron.ipcRenderer.invoke('open-directory', storagePath)
if (!result.success) {
message.error(`打开目录失败:${result.error}`)
}
} catch (error) {
console.error('打开存储目录失败:', error)
message.error(`打开目录失败:${error.message}`)
}
}
//
const handleSelectStoragePath = async () => {
try {
// 使 IPC
const result = await window.electron.ipcRenderer.invoke('select-directory', {
title: '选择存储目录',
defaultPath: storagePath
})
if (result.success && result.path) {
setStoragePath(result.path)
localStorage.setItem('storagePath', result.path)
message.success('存储目录设置成功!')
} else if (!result.canceled) {
message.error(`选择目录失败:${result.error}`)
}
} catch (error) {
console.error('选择存储目录失败:', error)
message.error(`选择目录失败:${error.message}`)
}
}
//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) {
@ -444,18 +642,44 @@ function SystemSettings() {
存储目录
</div>
<div>
<Button icon={<EyeOutlined />} type="text" disabled={!connectedDevice}></Button>
<Button icon={<EditOutlined />} type="text" disabled={!connectedDevice}></Button>
<Button
icon={<EyeOutlined />}
type="text"
onClick={handleOpenStoragePath}
title="打开存储目录"
/>
<Button
icon={<EditOutlined />}
type="text"
onClick={handleSelectStoragePath}
title="选择存储目录"
/>
</div>
</Flex>
<Flex align="center" gap={8} className={styles.storageInputRow}>
<Input className={styles.storageInput} disabled={!connectedDevice} />
<Input
className={styles.storageInput}
value={storagePath}
readOnly
placeholder="存储目录路径"
/>
</Flex>
<Checkbox className={styles.checkboxRight} disabled={!connectedDevice}>
<Checkbox
className={styles.checkboxRight}
disabled={!connectedDevice}
checked={realtimeDataEnabled}
onChange={(e) => setRealtimeDataEnabled(e.target.checked)}
>
实时数据
</Checkbox>
<Checkbox disabled={!connectedDevice}>报警数据</Checkbox>
<Checkbox
disabled={!connectedDevice}
checked={alarmDataEnabled}
onChange={(e) => setAlarmDataEnabled(e.target.checked)}
>
报警数据
</Checkbox>
</div>
</Flex>
)

Loading…
Cancel
Save