feat: add auto-update functionality with settings UI
- Implement electron-updater integration with IPC handlers for update operations - Add Pinia store for managing update state and user preferences - Create settings UI with version display, update controls, and auto-update toggles - Update IPC constants and preload API for update-related communication - Refactor preload invoke methods to use async IPC consistently - Add rounded corners to settings page layout for better visual consistency
This commit is contained in:
@@ -58,6 +58,13 @@ export enum IPC_EVENTS {
|
||||
|
||||
// 打开渠道
|
||||
OPEN_CHANNEL = 'open-channel',
|
||||
|
||||
// 更新
|
||||
UPDATE_CHECK = 'update:check',
|
||||
UPDATE_DOWNLOAD = 'update:download',
|
||||
UPDATE_INSTALL = 'update:install',
|
||||
UPDATE_VERSION = 'update:version',
|
||||
UPDATE_STATUS_CHANGED = 'update:status-changed',
|
||||
}
|
||||
|
||||
export const MAIN_WIN_SIZE = {
|
||||
@@ -82,6 +89,8 @@ export enum CONFIG_KEYS {
|
||||
MINIMIZE_TO_TRAY = 'minimizeToTray',
|
||||
PROVIDER = 'provider',
|
||||
DEFAULT_MODEL = 'defaultModel',
|
||||
AUTO_CHECK_UPDATE = 'autoCheckUpdate',
|
||||
AUTO_DOWNLOAD_UPDATE = 'autoDownloadUpdate',
|
||||
}
|
||||
|
||||
export enum MENU_IDS {
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<!-- 浅色主题 -->
|
||||
<button
|
||||
class="theme-button px-5 py-2.5 rounded-full border text-[14px] font-medium transition-all duration-200 flex items-center gap-2"
|
||||
class="theme-button px-5 py-1.5 rounded-full border text-[14px] font-medium transition-all duration-200 flex items-center gap-2"
|
||||
:class="[
|
||||
currentTheme === 'light'
|
||||
? 'bg-primary-50 dark:bg-primary-900/20 border-primary-500 dark:border-primary-400 text-primary-700 dark:text-primary-300'
|
||||
? 'bg-green-50 dark:bg-green-900/20 border-green-500 dark:border-green-400 text-green-700 dark:text-green-300'
|
||||
: 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'
|
||||
]" @click="handleThemeChange('light')">
|
||||
<RiSunLine class="w-4 h-4" />
|
||||
@@ -23,10 +23,10 @@
|
||||
|
||||
<!-- 深色主题 -->
|
||||
<button
|
||||
class="theme-button px-5 py-2.5 rounded-full border text-[14px] font-medium transition-all duration-200 flex items-center gap-2"
|
||||
class="theme-button px-5 py-1.5 rounded-full border text-[14px] font-medium transition-all duration-200 flex items-center gap-2"
|
||||
:class="[
|
||||
currentTheme === 'dark'
|
||||
? 'bg-primary-50 dark:bg-primary-900/20 border-primary-500 dark:border-primary-400 text-primary-700 dark:text-primary-300'
|
||||
? 'bg-green-50 dark:bg-green-900/20 border-green-500 dark:border-green-400 text-green-700 dark:text-green-300'
|
||||
: 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'
|
||||
]" @click="handleThemeChange('dark')">
|
||||
<RiMoonLine class="w-4 h-4" />
|
||||
@@ -35,10 +35,10 @@
|
||||
|
||||
<!-- 跟随系统 -->
|
||||
<button
|
||||
class="theme-button px-5 py-2.5 rounded-full border text-[14px] font-medium transition-all duration-200 flex items-center gap-2"
|
||||
class="theme-button px-5 py-1.5 rounded-full border text-[14px] font-medium transition-all duration-200 flex items-center gap-2"
|
||||
:class="[
|
||||
currentTheme === 'system'
|
||||
? 'bg-primary-50 dark:bg-primary-900/20 border-primary-500 dark:border-primary-400 text-primary-700 dark:text-primary-300'
|
||||
? 'bg-green-50 dark:bg-green-900/20 border-green-500 dark:border-green-400 text-green-700 dark:text-green-300'
|
||||
: 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'
|
||||
]" @click="handleThemeChange('system')">
|
||||
<RiComputerLine class="w-4 h-4" />
|
||||
@@ -62,12 +62,76 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 版本信息 -->
|
||||
<div
|
||||
class="w-full flex items-center mt-[20px] py-[20px] box-border border-b-[1px] border-dashed border-b-[#E5E8EE] dark:border-gray-700">
|
||||
<div class="label w-[64px] text-[16px] font-medium text-[#171717] dark:text-gray-100 mr-[24px]">当前版本</div>
|
||||
<div class="value text-[16px] font-medium text-[#171717] dark:text-gray-100">1.0.0</div>
|
||||
<el-button type="text" class="ml-auto">检查更新</el-button>
|
||||
<!-- 更新部分 -->
|
||||
<div class="mt-[40px]">
|
||||
<div class="text-[24px] font-medium text-[#171717] dark:text-gray-100 mb-[24px]">更新</div>
|
||||
|
||||
<!-- 当前版本 -->
|
||||
<div class="flex items-center justify-between mb-[16px]">
|
||||
<div>
|
||||
<div class="text-[14px] text-[#525866] dark:text-gray-400 mb-[4px]">当前版本</div>
|
||||
<div class="text-[28px] font-bold text-[#171717] dark:text-gray-100">v{{ updateStore.currentVersion }}</div>
|
||||
</div>
|
||||
<el-button link class="!p-[8px] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full"
|
||||
@click="updateStore.checkUpdate()">
|
||||
<RiRefreshLine class="w-5 h-5 text-[#525866] dark:text-gray-400"
|
||||
:class="{ 'animate-spin': updateStore.status === 'checking' }" />
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 更新状态卡片 -->
|
||||
<div class="bg-[#F5F7FA] dark:bg-gray-800 rounded-[8px] p-[16px] flex items-center justify-between mb-[16px]">
|
||||
<div class="text-[14px] text-[#525866] dark:text-gray-300">
|
||||
<span v-if="updateStore.status === 'checking'">正在检查更新...</span>
|
||||
<span v-else-if="updateStore.status === 'not-available'">您已拥有最新版本</span>
|
||||
<span v-else-if="updateStore.status === 'available'">发现新版本: v{{ updateStore.updateInfo?.version }}</span>
|
||||
<span v-else-if="updateStore.status === 'downloading'">
|
||||
正在下载新版本... {{ Math.round(updateStore.progress?.percent || 0) }}%
|
||||
</span>
|
||||
<span v-else-if="updateStore.status === 'downloaded'">下载完成,准备安装</span>
|
||||
<span v-else-if="updateStore.status === 'error'" class="text-red-500">更新出错: {{ updateStore.error }}</span>
|
||||
<span v-else>检查更新以获取最新功能</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-button v-if="updateStore.status === 'available'" type="primary" @click="updateStore.downloadUpdate()">
|
||||
下载更新
|
||||
</el-button>
|
||||
<el-button v-else-if="updateStore.status === 'downloaded'" type="success"
|
||||
@click="updateStore.installUpdate()">
|
||||
重启安装
|
||||
</el-button>
|
||||
<el-button v-else
|
||||
class="!bg-white dark:!bg-gray-700 !border-[#E5E8EE] dark:!border-gray-600 !text-[#171717] dark:!text-gray-100"
|
||||
@click="updateStore.checkUpdate()">
|
||||
<RiRefreshLine class="w-4 h-4 mr-[4px]" :class="{ 'animate-spin': updateStore.status === 'checking' }" />
|
||||
检查更新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-[12px] text-[#99A0AE] dark:text-gray-500 mb-[32px]">
|
||||
开启自动更新后,更新将自动下载并安装。
|
||||
</div>
|
||||
|
||||
<!-- 自动检查更新 -->
|
||||
<div class="flex items-center justify-between py-[16px] border-b-[1px] border-[#E5E8EE] dark:border-gray-800">
|
||||
<div>
|
||||
<div class="text-[16px] text-[#171717] dark:text-gray-100 mb-[4px]">自动检查更新</div>
|
||||
<div class="text-[14px] text-[#99A0AE] dark:text-gray-500">启动时检查更新</div>
|
||||
</div>
|
||||
<el-switch v-model="updateStore.autoCheckUpdate" @change="updateStore.setAutoCheckUpdate" />
|
||||
</div>
|
||||
|
||||
<!-- 自动更新 -->
|
||||
<div class="flex items-center justify-between py-[16px]">
|
||||
<div>
|
||||
<div class="text-[16px] text-[#171717] dark:text-gray-100 mb-[4px]">自动更新</div>
|
||||
<div class="text-[14px] text-[#99A0AE] dark:text-gray-500">自动下载并安装更新</div>
|
||||
</div>
|
||||
<el-switch v-model="updateStore.autoDownloadUpdate" @change="updateStore.setAutoDownloadUpdate" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -77,7 +141,17 @@ import { ref, onMounted, computed } from 'vue'
|
||||
import TitleSection from '@src/components/TitleSection/index.vue'
|
||||
import { setLanguage, getLanguage, type LanguageType } from '@src/i18n'
|
||||
import { useThemeStore } from '@src/stores/theme'
|
||||
import { RiSunLine, RiMoonLine, RiComputerLine } from '@remixicon/vue'
|
||||
import { useUpdateStore } from '@src/stores/update'
|
||||
import { RiSunLine, RiMoonLine, RiComputerLine, RiRefreshLine } from '@remixicon/vue'
|
||||
|
||||
// 更新状态管理
|
||||
const updateStore = useUpdateStore()
|
||||
|
||||
const getStatusText = computed(() => {
|
||||
if (updateStore.status === 'checking') return '正在检查...';
|
||||
if (updateStore.status === 'not-available') return '已是最新版';
|
||||
return '检查更新';
|
||||
});
|
||||
|
||||
// 主题状态管理
|
||||
const themeStore = useThemeStore()
|
||||
@@ -105,6 +179,7 @@ const currentLanguage = ref<LanguageType>('zh')
|
||||
|
||||
onMounted(() => {
|
||||
currentLanguage.value = getLanguage() as LanguageType
|
||||
updateStore.init()
|
||||
})
|
||||
|
||||
const handleLanguageChange = async (langCode: LanguageType) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<layout>
|
||||
<div class="bg-white box-border w-full h-full flex">
|
||||
<div class="bg-white box-border w-full h-full flex rounded-[8px]">
|
||||
<SystemConfig @change=onChange />
|
||||
<component :is="currentComponent" />
|
||||
</div>
|
||||
|
||||
72
src/stores/update.ts
Normal file
72
src/stores/update.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { IPC_EVENTS, CONFIG_KEYS } from '@lib/constants';
|
||||
|
||||
export const useUpdateStore = defineStore('update', {
|
||||
state: () => ({
|
||||
status: 'idle' as 'idle' | 'checking' | 'available' | 'downloading' | 'downloaded' | 'error' | 'not-available',
|
||||
currentVersion: '1.0.0',
|
||||
updateInfo: null as any,
|
||||
progress: null as any,
|
||||
error: null as string | null,
|
||||
initialized: false,
|
||||
autoCheckUpdate: true,
|
||||
autoDownloadUpdate: false,
|
||||
}),
|
||||
actions: {
|
||||
async init() {
|
||||
if (this.initialized) return;
|
||||
this.initialized = true;
|
||||
|
||||
try {
|
||||
this.currentVersion = await window.api.invoke(IPC_EVENTS.UPDATE_VERSION);
|
||||
|
||||
// Initialize config
|
||||
const checkUpdate = await window.api.invoke(IPC_EVENTS.GET_CONFIG, CONFIG_KEYS.AUTO_CHECK_UPDATE);
|
||||
if (checkUpdate !== undefined) this.autoCheckUpdate = checkUpdate;
|
||||
|
||||
const downloadUpdate = await window.api.invoke(IPC_EVENTS.GET_CONFIG, CONFIG_KEYS.AUTO_DOWNLOAD_UPDATE);
|
||||
if (downloadUpdate !== undefined) this.autoDownloadUpdate = downloadUpdate;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to init update store:', error);
|
||||
}
|
||||
|
||||
window.api.on(IPC_EVENTS.UPDATE_STATUS_CHANGED, (data: any) => {
|
||||
this.status = data.status;
|
||||
if (data.info) this.updateInfo = data.info;
|
||||
if (data.progress) this.progress = data.progress;
|
||||
if (data.error) this.error = data.error;
|
||||
});
|
||||
},
|
||||
async setAutoCheckUpdate(val: boolean) {
|
||||
this.autoCheckUpdate = val;
|
||||
window.api.send(IPC_EVENTS.UPDATE_CONFIG, { [CONFIG_KEYS.AUTO_CHECK_UPDATE]: val });
|
||||
},
|
||||
async setAutoDownloadUpdate(val: boolean) {
|
||||
this.autoDownloadUpdate = val;
|
||||
window.api.send(IPC_EVENTS.UPDATE_CONFIG, { [CONFIG_KEYS.AUTO_DOWNLOAD_UPDATE]: val });
|
||||
},
|
||||
async checkUpdate() {
|
||||
this.error = null;
|
||||
this.status = 'checking';
|
||||
try {
|
||||
await window.api.invoke(IPC_EVENTS.UPDATE_CHECK);
|
||||
} catch (error) {
|
||||
this.status = 'error';
|
||||
this.error = String(error);
|
||||
}
|
||||
},
|
||||
async downloadUpdate() {
|
||||
this.status = 'downloading';
|
||||
try {
|
||||
await window.api.invoke(IPC_EVENTS.UPDATE_DOWNLOAD);
|
||||
} catch (error) {
|
||||
this.status = 'error';
|
||||
this.error = String(error);
|
||||
}
|
||||
},
|
||||
installUpdate() {
|
||||
window.api.invoke(IPC_EVENTS.UPDATE_INSTALL);
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user