Prechádzať zdrojové kódy

增加自动更新功能

快乐的梦鱼 1 týždeň pred
rodič
commit
1903b6cca5

+ 7 - 0
.claude/settings.local.json

@@ -0,0 +1,7 @@
+{
+  "permissions": {
+    "allow": [
+      "Bash(npx vue-tsc *)"
+    ]
+  }
+}

+ 3 - 0
electron/main.ts

@@ -2,6 +2,7 @@ import { app, BrowserWindow, dialog, Event, Input, ipcMain, Menu, WebContentsVie
 import { fileURLToPath } from 'node:url'
 import path from 'node:path'
 import fs from 'node:fs'
+import { initUpdater } from './updater'
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url))
 
@@ -172,6 +173,8 @@ function createWindow() {
   loadViewUrl(expandButtonView, '/expand');
   updateChildWindowBounds();
 
+  initUpdater(mainWindow);
+
   mainWindow.on('resize', () => {
     updateChildWindowBounds()
   })

+ 53 - 0
electron/updater.ts

@@ -0,0 +1,53 @@
+import { autoUpdater } from 'electron-updater'
+import { BrowserWindow, ipcMain } from 'electron'
+
+let mainWindow: BrowserWindow | null = null
+
+export function initUpdater(win: BrowserWindow) {
+  mainWindow = win
+
+  autoUpdater.autoDownload = false
+  autoUpdater.autoInstallOnAppQuit = true
+
+  autoUpdater.on('checking-for-update', () => {
+    sendUpdateStatus('checking')
+  })
+  autoUpdater.on('update-available', (info) => {
+    sendUpdateStatus('available', {
+      version: info.version,
+      releaseNotes: info.releaseNotes,
+    })
+  })
+  autoUpdater.on('update-not-available', () => {
+    sendUpdateStatus('not-available')
+  })
+  autoUpdater.on('download-progress', (progress) => {
+    sendUpdateStatus('downloading', {
+      percent: Math.round(progress.percent),
+      transferred: progress.transferred,
+      total: progress.total,
+    })
+  })
+  autoUpdater.on('update-downloaded', () => {
+    sendUpdateStatus('downloaded')
+  })
+  autoUpdater.on('error', (err) => {
+    sendUpdateStatus('error', { message: err.message })
+  })
+
+  ipcMain.on('updater-check', () => {
+    autoUpdater.checkForUpdates()
+  })
+  ipcMain.on('updater-download', () => {
+    autoUpdater.downloadUpdate()
+  })
+  ipcMain.on('updater-install', () => {
+    autoUpdater.quitAndInstall()
+  })
+
+  autoUpdater.checkForUpdates()
+}
+
+function sendUpdateStatus(status: string, data?: any) {
+  mainWindow?.webContents.send('updater-status', { status, ...data })
+}

+ 98 - 9
package-lock.json

@@ -1,18 +1,19 @@
 {
   "name": "minnan-demo-app",
-  "version": "0.0.0",
+  "version": "1.0.1",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "minnan-demo-app",
-      "version": "0.0.0",
+      "version": "1.0.1",
       "dependencies": {
         "@guolao/vue-monaco-editor": "^1.6.0",
         "@imengyu/imengyu-utils": "^0.0.25",
         "@imengyu/vue-scroll-rect": "^0.1.8",
         "@imengyu/vue3-context-menu": "^1.5.3",
         "ant-design-vue": "^4.2.6",
+        "electron-updater": "^6.8.9",
         "vue": "^3.4.21",
         "vue-router": "^4.6.4"
       },
@@ -2459,7 +2460,6 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
-      "dev": true,
       "license": "Python-2.0"
     },
     "node_modules/array-tree-filter": {
@@ -3146,7 +3146,6 @@
       "version": "4.4.3",
       "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
       "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "ms": "^2.1.3"
@@ -3654,6 +3653,82 @@
         "node": ">= 10.0.0"
       }
     },
+    "node_modules/electron-updater": {
+      "version": "6.8.9",
+      "resolved": "https://registry.npmmirror.com/electron-updater/-/electron-updater-6.8.9.tgz",
+      "integrity": "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==",
+      "license": "MIT",
+      "dependencies": {
+        "builder-util-runtime": "9.7.0",
+        "fs-extra": "^10.1.0",
+        "js-yaml": "^4.1.0",
+        "lazy-val": "^1.0.5",
+        "lodash.escaperegexp": "^4.1.2",
+        "lodash.isequal": "^4.5.0",
+        "semver": "~7.7.3",
+        "tiny-typed-emitter": "^2.1.0"
+      }
+    },
+    "node_modules/electron-updater/node_modules/builder-util-runtime": {
+      "version": "9.7.0",
+      "resolved": "https://registry.npmmirror.com/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz",
+      "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.3.4",
+        "sax": "^1.2.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/electron-updater/node_modules/fs-extra": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz",
+      "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/electron-updater/node_modules/jsonfile": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz",
+      "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
+      "license": "MIT",
+      "dependencies": {
+        "universalify": "^2.0.0"
+      },
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "node_modules/electron-updater/node_modules/semver": {
+      "version": "7.7.4",
+      "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz",
+      "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/electron-updater/node_modules/universalify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz",
+      "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
     "node_modules/emoji-regex": {
       "version": "8.0.0",
       "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -4212,7 +4287,6 @@
       "version": "4.2.11",
       "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
       "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
-      "dev": true,
       "license": "ISC"
     },
     "node_modules/has-flag": {
@@ -4561,7 +4635,6 @@
       "version": "4.1.1",
       "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz",
       "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "argparse": "^2.0.1"
@@ -4629,7 +4702,6 @@
       "version": "1.0.5",
       "resolved": "https://registry.npmmirror.com/lazy-val/-/lazy-val-1.0.5.tgz",
       "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/lazystream": {
@@ -4710,6 +4782,12 @@
       "license": "MIT",
       "peer": true
     },
+    "node_modules/lodash.escaperegexp": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmmirror.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
+      "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
+      "license": "MIT"
+    },
     "node_modules/lodash.flatten": {
       "version": "4.4.0",
       "resolved": "https://registry.npmmirror.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
@@ -4718,6 +4796,13 @@
       "license": "MIT",
       "peer": true
     },
+    "node_modules/lodash.isequal": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+      "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+      "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
+      "license": "MIT"
+    },
     "node_modules/lodash.isplainobject": {
       "version": "4.0.6",
       "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
@@ -4949,7 +5034,6 @@
       "version": "2.1.3",
       "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
       "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/muggle-string": {
@@ -5850,7 +5934,6 @@
       "version": "1.4.4",
       "resolved": "https://registry.npmmirror.com/sax/-/sax-1.4.4.tgz",
       "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
-      "dev": true,
       "license": "BlueOak-1.0.0",
       "engines": {
         "node": ">=11.0.0"
@@ -6268,6 +6351,12 @@
         "node": ">=12.22"
       }
     },
+    "node_modules/tiny-typed-emitter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
+      "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
+      "license": "MIT"
+    },
     "node_modules/tmp": {
       "version": "0.2.5",
       "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.5.tgz",

+ 8 - 1
package.json

@@ -17,6 +17,7 @@
     "@imengyu/vue-scroll-rect": "^0.1.8",
     "@imengyu/vue3-context-menu": "^1.5.3",
     "ant-design-vue": "^4.2.6",
+    "electron-updater": "^6.8.9",
     "vue": "^3.4.21",
     "vue-router": "^4.6.4"
   },
@@ -31,5 +32,11 @@
     "vite-plugin-electron-renderer": "^0.14.5",
     "vue-tsc": "^2.0.26"
   },
-  "main": "dist-electron/main.js"
+  "main": "dist-electron/main.js",
+  "build": {
+    "publish": {
+      "provider": "generic",
+      "url": "https://mn.wenlvti.net/app_static/minnan-demo/releases/"
+    }
+  }
 }

+ 0 - 12
src/components/Update.vue

@@ -1,12 +0,0 @@
-<template>
-
-</template>
-
-<script setup lang="ts">
-function checkUpdate() {
-}
-</script>
-
-<style scoped>
-
-</style>

+ 133 - 0
src/components/UpdateStatus.vue

@@ -0,0 +1,133 @@
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted } from 'vue'
+
+type UpdateState = 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error'
+
+const state = ref<UpdateState>('idle')
+const version = ref('')
+const percent = ref(0)
+const errorMsg = ref('')
+const visible = ref(false)
+
+function onStatus(_event: any, data: any) {
+  state.value = data.status
+  if (data.status === 'available') {
+    version.value = data.version || ''
+    visible.value = true
+  } else if (data.status === 'downloading') {
+    percent.value = data.percent || 0
+    visible.value = true
+  } else if (data.status === 'downloaded') {
+    visible.value = true
+  } else if (data.status === 'error') {
+    errorMsg.value = data.message || '更新失败'
+    visible.value = true
+  } else if (data.status === 'not-available') {
+    visible.value = false
+  } else if (data.status === 'checking') {
+    visible.value = false
+  }
+}
+
+function startDownload() {
+  window.ipcRenderer.send('updater-download')
+}
+function installNow() {
+  window.ipcRenderer.send('updater-install')
+}
+function dismiss() {
+  visible.value = false
+}
+function checkUpdate() {
+  window.ipcRenderer.send('updater-check')
+}
+
+onMounted(() => {
+  window.ipcRenderer.on('updater-status', onStatus)
+})
+onUnmounted(() => {
+  window.ipcRenderer.off('updater-status', onStatus)
+})
+</script>
+
+<template>
+  <div v-if="visible" class="update-status">
+    <!-- 有新版本可用 -->
+    <template v-if="state === 'available'">
+      <span class="update-text">新版本 v{{ version }} 可用</span>
+      <button class="update-btn" @click="startDownload">下载</button>
+      <button class="update-btn dismiss" @click="dismiss">忽略</button>
+    </template>
+    <!-- 下载中 -->
+    <template v-else-if="state === 'downloading'">
+      <span class="update-text">下载中 {{ percent }}%</span>
+      <div class="progress-bar">
+        <div class="progress-fill" :style="{ width: percent + '%' }"></div>
+      </div>
+    </template>
+    <!-- 下载完成 -->
+    <template v-else-if="state === 'downloaded'">
+      <span class="update-text">下载完成,重启以安装</span>
+      <button class="update-btn" @click="installNow">立即重启</button>
+      <button class="update-btn dismiss" @click="dismiss">稍后</button>
+    </template>
+    <!-- 错误 -->
+    <template v-else-if="state === 'error'">
+      <span class="update-text error">更新失败: {{ errorMsg }}</span>
+      <button class="update-btn" @click="checkUpdate">重试</button>
+      <button class="update-btn dismiss" @click="dismiss">关闭</button>
+    </template>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.update-status {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 12px;
+  margin: 0 12px 8px;
+  background: #1a3a5c;
+  border-radius: 6px;
+  font-size: 12px;
+  flex-wrap: wrap;
+}
+.update-text {
+  color: #e0e0e0;
+  &.error {
+    color: #ff6b6b;
+  }
+}
+.update-btn {
+  padding: 3px 10px;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 12px;
+  background: #4a9eff;
+  color: #fff;
+  &:hover {
+    background: #3a8eef;
+  }
+  &.dismiss {
+    background: transparent;
+    color: #aaa;
+    &:hover {
+      color: #fff;
+    }
+  }
+}
+.progress-bar {
+  flex: 1;
+  height: 6px;
+  background: #2a4a6c;
+  border-radius: 3px;
+  min-width: 80px;
+}
+.progress-fill {
+  height: 100%;
+  background: #4a9eff;
+  border-radius: 3px;
+  transition: width 0.3s;
+}
+</style>

+ 2 - 1
src/views/Main.vue

@@ -5,7 +5,7 @@ import { HtmlUtils } from '@imengyu/imengyu-utils'
 import { AppItem } from '../model/App'
 import AppList from '../components/AppList.vue'
 import AppListItem from '../components/AppListItem.vue'
-import { Modal, message } from 'ant-design-vue'
+import UpdateStatus from '../components/UpdateStatus.vue'
 
 // 状态管理
 const apps = ref<Record<string, AppItem[]>>({})
@@ -161,6 +161,7 @@ onMounted(() => {
           <img src="../assets/menu.svg" alt="菜单" />
         </button>
       </h2>
+      <UpdateStatus />
       <AppList>
         <div v-for="(group, title) in apps" :key="title">
           <h5>{{ title }}</h5>