Browse Source

feat: 添加自动更新功能,整合更新管理器和UI组件

master
cles 2 weeks ago
parent
commit
fbea6ce5d0
  1. 2
      electron-builder.yml
  2. 76
      package-lock.json
  3. 3
      package.json
  4. 121
      src/main/autoUpdaterManager.js
  5. 17
      src/main/index.js
  6. 2
      src/renderer/src/App.jsx
  7. 11
      src/renderer/src/common/ipcEvents.js
  8. 189
      src/renderer/src/components/AutoUpdater/AutoUpdater.jsx
  9. 47
      src/renderer/src/components/AutoUpdater/AutoUpdater.module.css

2
electron-builder.yml

@ -41,6 +41,6 @@ appImage:
npmRebuild: false
publish:
provider: generic
url: https://gitee.com/bocinpity/flexometer-setup/releases/download/v${version}
url: https://gitee.com/bocinpity/flexometer-setup/releases/download/latest
electronDownload:
mirror: https://npmmirror.com/mirrors/electron/

76
package-lock.json

@ -1,18 +1,17 @@
{
"name": "FlexometerSetup",
"version": "1.0.0",
"version": "1.0.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "FlexometerSetup",
"version": "1.0.0",
"version": "1.0.5",
"hasInstallScript": true,
"dependencies": {
"@ant-design/icons": "^5.6.1",
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"ahooks": "^3.9.5",
"antd": "^5.27.1",
"chart.js": "^4.5.0",
"electron-log": "^5.4.3",
@ -2460,12 +2459,6 @@
"integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
"license": "MIT"
},
"node_modules/@types/js-cookie": {
"version": "3.0.6",
"resolved": "https://registry.npmmirror.com/@types/js-cookie/-/js-cookie-3.0.6.tgz",
"integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz",
@ -2661,31 +2654,6 @@
"node": ">=8"
}
},
"node_modules/ahooks": {
"version": "3.9.5",
"resolved": "https://registry.npmmirror.com/ahooks/-/ahooks-3.9.5.tgz",
"integrity": "sha512-TrjXie49Q8HuHKTa84Fm9A+famMDAG1+7a9S9Gq6RQ0h90Jgqmiq3CkObuRjWT/C4d6nRZCw35Y2k2fmybb5eA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.21.0",
"@types/js-cookie": "^3.0.6",
"dayjs": "^1.9.1",
"intersection-observer": "^0.12.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"react-fast-compare": "^3.2.2",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^5.0.0",
"tslib": "^2.4.1"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
@ -6299,12 +6267,6 @@
"node": ">= 0.4"
}
},
"node_modules/intersection-observer": {
"version": "0.12.2",
"resolved": "https://registry.npmmirror.com/intersection-observer/-/intersection-observer-0.12.2.tgz",
"integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==",
"license": "Apache-2.0"
},
"node_modules/ip-address": {
"version": "10.0.1",
"resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.0.1.tgz",
@ -6840,15 +6802,6 @@
"node": ">=10"
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
@ -7081,6 +7034,7 @@
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.defaults": {
@ -8914,12 +8868,6 @@
"react": "^18.3.1"
}
},
"node_modules/react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
"license": "MIT"
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz",
@ -9380,18 +9328,6 @@
"loose-envify": "^1.1.0"
}
},
"node_modules/screenfull": {
"version": "5.2.0",
"resolved": "https://registry.npmmirror.com/screenfull/-/screenfull-5.2.0.tgz",
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/scroll-into-view-if-needed": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
@ -10181,12 +10117,6 @@
"utf8-byte-length": "^1.0.1"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",

3
package.json

@ -1,6 +1,6 @@
{
"name": "FlexometerSetup",
"version": "1.0.1",
"version": "1.0.7",
"description": "An Electron application with React",
"main": "./out/main/index.js",
"author": "cles",
@ -21,7 +21,6 @@
"@ant-design/icons": "^5.6.1",
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"ahooks": "^3.9.5",
"antd": "^5.27.1",
"chart.js": "^4.5.0",
"electron-log": "^5.4.3",

121
src/main/autoUpdaterManager.js

@ -0,0 +1,121 @@
import { autoUpdater } from 'electron-updater'
import { ipcMain } from 'electron'
import { is } from '@electron-toolkit/utils'
import log from 'electron-log'
import { IPC_EVENT } from '../renderer/src/common/ipcEvents'
class AutoUpdaterManager {
constructor() {
this.mainWindow = null
}
/**
* 初始化自动更新管理器
* @param {BrowserWindow} mainWindow - 主窗口实例
*/
initialize(mainWindow) {
this.mainWindow = mainWindow
// 只在生产环境启用自动更新
if (!is.dev) {
// 禁用自动下载,让用户手动选择
autoUpdater.autoDownload = false
this.setupUpdateListeners()
this.setupIpcHandlers()
this.checkForUpdates()
}
}
/**
* 检查更新
*/
checkForUpdates() {
try {
log.info('开始检查更新')
autoUpdater.checkForUpdates()
} catch (e) {
log.error('自动更新失败', e)
}
}
/**
* 设置自动更新事件监听器
*/
setupUpdateListeners() {
// 发现新版本
autoUpdater.on('update-available', (info) => {
log.info('发现新版本,通知渲染进程')
this.mainWindow.webContents.send(IPC_EVENT.UPDATE_AVAILABLE, info)
})
// 没有更新
autoUpdater.on('update-not-available', () => {
log.info('当前为最新版本')
this.mainWindow.webContents.send(IPC_EVENT.UPDATE_NOT_AVAILABLE)
})
// 下载进度
autoUpdater.on('download-progress', (progressObj) => {
log.info(`下载进度: ${Math.round(progressObj.percent)}%`)
this.mainWindow.webContents.send(IPC_EVENT.DOWNLOAD_PROGRESS, progressObj)
})
// 更新出错
autoUpdater.on('error', (err) => {
log.error('更新出错', err)
this.mainWindow.webContents.send(IPC_EVENT.UPDATE_ERROR, err.message)
})
// 下载完成
autoUpdater.on('update-downloaded', () => {
log.info('新版本下载完成,通知渲染进程')
this.mainWindow.webContents.send(IPC_EVENT.UPDATE_DOWNLOADED)
})
}
/**
* 设置IPC处理程序
*/
setupIpcHandlers() {
// 监听渲染进程的更新下载请求
ipcMain.handle(IPC_EVENT.UPDATE_DOWNLOAD, () => {
log.info('渲染进程请求开始下载更新')
autoUpdater.downloadUpdate()
})
// 监听渲染进程的更新安装请求
ipcMain.handle(IPC_EVENT.UPDATE_INSTALL, () => {
log.info('渲染进程请求安装更新')
autoUpdater.quitAndInstall()
})
}
/**
* 手动触发检查更新
*/
manualCheckForUpdates() {
if (!is.dev) {
this.checkForUpdates()
} else {
log.info('开发环境,跳过更新检查')
}
}
/**
* 清理资源
*/
cleanup() {
// 移除所有监听器
autoUpdater.removeAllListeners()
// 移除IPC处理程序
ipcMain.removeHandler(IPC_EVENT.UPDATE_DOWNLOAD)
ipcMain.removeHandler(IPC_EVENT.UPDATE_INSTALL)
}
}
// 创建单例实例
const autoUpdaterManager = new AutoUpdaterManager()
export default autoUpdaterManager

17
src/main/index.js

@ -4,8 +4,7 @@ import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
import { registerIpRouter } from './ipcRouter'
import log from 'electron-log'
import { autoUpdater } from 'electron-updater'
import autoUpdaterManager from './autoUpdaterManager'
// 开发环境日志写到项目根目录 logs 文件夹
if (is.dev) {
log.transports.file.resolvePathFn = () => `${process.cwd()}/logs/main.log`
@ -37,6 +36,8 @@ function createWindow() {
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
return mainWindow
}
app.whenReady().then(() => {
@ -48,17 +49,19 @@ app.whenReady().then(() => {
})
// IPC处理函数注册
registerIpRouter()
createWindow()
// 自动更新监测, 生产环境使用
if (!is.dev) {
autoUpdater.checkForUpdatesAndNotify()
}
const mainWindow = createWindow()
// 初始化自动更新管理器
autoUpdaterManager.initialize(mainWindow)
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
// 清理自动更新管理器资源
autoUpdaterManager.cleanup()
if (process.platform !== 'darwin') {
app.quit()
}

2
src/renderer/src/App.jsx

@ -2,6 +2,7 @@ import { Layout } from 'antd'
import styles from './App.module.css'
const { Sider, Content } = Layout
import SiderHeader from './components/SiderHeader/SiderHeader'
import AutoUpdater from './components/AutoUpdater/AutoUpdater'
import { FundFilled, CameraFilled } from '@ant-design/icons'
import { Tabs } from 'antd'
import DeflectionCollection from './components/DeflectionCollection/DeflectionCollection'
@ -48,6 +49,7 @@ const App = () => {
/>
</Content>
</Layout>
<AutoUpdater />
</Layout>
)
}

11
src/renderer/src/common/ipcEvents.js

@ -31,5 +31,14 @@ export const IPC_EVENT = {
DATA_FPS_SET: 'dataFps:set',
VIDEO_FPS_SET: 'videoFps:set',
THRESHOLD_SET: 'threshold:set',
INVALID_DATA_COUNT_SET: 'invalidDataCount:set'
INVALID_DATA_COUNT_SET: 'invalidDataCount:set',
// 自动更新相关的value不要乱改,定义在这里是为了方便,这些都是标准的api事件名
UPDATE_AVAILABLE: 'update-available',
UPDATE_NOT_AVAILABLE: 'update-not-available',
DOWNLOAD_PROGRESS: 'download-progress',
UPDATE_DOWNLOADED: 'update-downloaded',
UPDATE_ERROR: 'update-error',
UPDATE_DOWNLOAD: 'update-download',
UPDATE_INSTALL: 'update-install'
}

189
src/renderer/src/components/AutoUpdater/AutoUpdater.jsx

@ -0,0 +1,189 @@
import { useEffect, useState } from 'react'
import { Modal, Progress, Button, Space, Typography, notification } from 'antd'
import { DownloadOutlined, ReloadOutlined, CloseOutlined } from '@ant-design/icons'
import { IPC_EVENT } from '../../common/ipcEvents'
import styles from './AutoUpdater.module.css'
const { Title, Text } = Typography
function AutoUpdater() {
const [updateInfo, setUpdateInfo] = useState(null)
const [isModalVisible, setIsModalVisible] = useState(false)
const [updateStatus, setUpdateStatus] = useState('')
const [downloadProgress, setDownloadProgress] = useState(0)
const [isDownloading, setIsDownloading] = useState(false)
const [isDownloaded, setIsDownloaded] = useState(false)
useEffect(() => {
//
const handleUpdateAvailable = (event, info) => {
console.log('收到update-available事件:', info)
setUpdateInfo(info)
setUpdateStatus('发现新版本')
setIsModalVisible(true)
setIsDownloading(false)
setIsDownloaded(false)
setDownloadProgress(0)
}
//
const handleDownloadProgress = (event, progressObj) => {
console.log('下载进度:', progressObj)
setDownloadProgress(Math.round(progressObj.percent))
setUpdateStatus(`正在下载新版本... ${Math.round(progressObj.percent)}%`)
}
//
const handleUpdateDownloaded = () => {
console.log('下载完成')
setIsDownloading(false)
setIsDownloaded(true)
setUpdateStatus('新版本下载完成,准备安装')
notification.success({
message: '更新下载完成',
description: '新版本已下载完成,点击"立即安装"重启应用以完成更新',
duration: 5
})
}
//
const handleUpdateError = (event, error) => {
console.error('更新错误:', error)
setIsDownloading(false)
setUpdateStatus(`更新失败: ${error}`)
notification.error({
message: '更新失败',
description: error,
duration: 5
})
}
//
window.electron.ipcRenderer.on(IPC_EVENT.UPDATE_AVAILABLE, handleUpdateAvailable)
window.electron.ipcRenderer.on(IPC_EVENT.DOWNLOAD_PROGRESS, handleDownloadProgress)
window.electron.ipcRenderer.on(IPC_EVENT.UPDATE_DOWNLOADED, handleUpdateDownloaded)
window.electron.ipcRenderer.on(IPC_EVENT.UPDATE_ERROR, handleUpdateError)
//
return () => {
window.electron.ipcRenderer.removeListener(IPC_EVENT.UPDATE_AVAILABLE, handleUpdateAvailable)
window.electron.ipcRenderer.removeListener(
IPC_EVENT.DOWNLOAD_PROGRESS,
handleDownloadProgress
)
window.electron.ipcRenderer.removeListener(
IPC_EVENT.UPDATE_DOWNLOADED,
handleUpdateDownloaded
)
window.electron.ipcRenderer.removeListener(IPC_EVENT.UPDATE_ERROR, handleUpdateError)
}
}, [])
//
const handleDownload = async () => {
setIsDownloading(true)
setUpdateStatus('准备下载新版本...')
setDownloadProgress(0)
try {
await window.electron.ipcRenderer.invoke(IPC_EVENT.UPDATE_DOWNLOAD)
} catch (error) {
console.error('请求下载失败:', error)
setIsDownloading(false)
}
}
//
const handleInstall = async () => {
try {
await window.electron.ipcRenderer.invoke(IPC_EVENT.UPDATE_INSTALL)
} catch (error) {
console.error('安装更新失败:', error)
}
}
//
const handleCancel = () => {
setIsModalVisible(false)
setUpdateInfo(null)
setIsDownloading(false)
setIsDownloaded(false)
setDownloadProgress(0)
}
return (
<Modal
title={
<Space>
<DownloadOutlined />
<span>软件更新</span>
</Space>
}
open={isModalVisible}
onCancel={handleCancel}
footer={null}
width={480}
maskClosable={false}
>
<div className={styles.modalContent}>
{updateInfo && (
<div className={styles.versionInfo}>
<Title level={5} className={styles.versionTitle}>
发现新版本v{updateInfo.version}
</Title>
{updateInfo.releaseNotes && (
<div className={styles.releaseNotesSection}>
<Text type="secondary">更新说明</Text>
<div className={styles.releaseNotesContainer}>
<Text className={styles.releaseNotesText}>{updateInfo.releaseNotes}</Text>
</div>
</div>
)}
</div>
)}
<div className={styles.statusSection}>
<Text>{updateStatus}</Text>
</div>
{isDownloading && (
<div className={styles.progressSection}>
<Progress
percent={downloadProgress}
status="active"
strokeColor={{
'0%': '#108ee9',
'100%': '#87d068'
}}
format={(percent) => `${percent}%`}
/>
</div>
)}
<div className={styles.actionButtons}>
<Space>
<Button icon={<CloseOutlined />} onClick={handleCancel}>
{isDownloaded ? '稍后安装' : '取消'}
</Button>
{!isDownloaded && !isDownloading && (
<Button type="primary" icon={<DownloadOutlined />} onClick={handleDownload}>
立即下载
</Button>
)}
{isDownloaded && (
<Button
type="primary"
icon={<ReloadOutlined />}
onClick={handleInstall}
className={styles.installButton}
>
立即安装
</Button>
)}
</Space>
</div>
</div>
</Modal>
)
}
export default AutoUpdater

47
src/renderer/src/components/AutoUpdater/AutoUpdater.module.css

@ -0,0 +1,47 @@
.modalContent {
padding: 16px 0;
}
.versionInfo {
margin-bottom: 16px;
}
.versionTitle {
margin: 0 !important;
margin-bottom: 8px !important;
}
.releaseNotesSection {
margin-bottom: 16px;
}
.releaseNotesContainer {
background: #f6f8fa;
padding: 12px;
border-radius: 6px;
margin-top: 8px;
max-height: 120px;
overflow-y: auto;
}
.releaseNotesText {
font-size: 12px;
line-height: 1.5;
}
.statusSection {
margin-bottom: 16px;
}
.progressSection {
margin-bottom: 16px;
}
.actionButtons {
text-align: right;
}
.installButton {
background-color: #52c41a !important;
border-color: #52c41a !important;
}
Loading…
Cancel
Save