Electron Forge【實戰】桌面應用 —— 將項目配置保存到本地

最終效果

在這里插入圖片描述
在這里插入圖片描述

定義默認配置

src/initData.ts

export const DEFAULT_CONFIG: AppConfig = {language: "zh",fontSize: 14,providerConfigs: {},
};

src/types.ts

export interface AppConfig {language: 'zh' | 'en'fontSize: numberproviderConfigs: Record<string, Record<string, string>>
}

從本地加載配置

因讀取配置文件需要時間,在創建主窗口前,便開始加載

src/main.ts

import { configManager } from './config'
const createWindow = async () => {// 加載配置await configManager.load();

src/config.ts

import { app } from "electron";
import path from "path";
import fs from "fs/promises";
import { AppConfig } from "./types";
import { DEFAULT_CONFIG } from "./initData";// 配置文件路徑,在windows 中是 C:\Users\用戶名\AppData\Roaming\項目名\config.json
const configPath = path.join(app.getPath("userData"), "config.json");
let config = { ...DEFAULT_CONFIG };export const configManager = {async load() {try {const data = await fs.readFile(configPath, "utf-8");config = { ...DEFAULT_CONFIG, ...JSON.parse(data) };} catch {await this.save();}return config;},async save() {await fs.writeFile(configPath, JSON.stringify(config, null, 2));return config;},async update(newConfig: Partial<AppConfig>) {config = { ...config, ...newConfig };await this.save();return config;},get() {return config;},
};

主進程中使用配置

直接調用 configManager 的 get 方法即可

src/providers/createProvider.ts

import { configManager } from "../config";
  const config = configManager.get();const providerConfig = config.providerConfigs[providerName] || {};

渲染進程中使用配置

需借助 electron 的 IPC 通信從主進程中獲取

src/views/Settings.vue

onMounted(async () => {const config = await (window as any).electronAPI.getConfig();
});

src/preload.ts

contextBridge.exposeInMainWorld("electronAPI", {startChat: (data: CreateChatProps) => ipcRenderer.send("start-chat", data),onUpdateMessage: (callback: OnUpdatedCallback) =>ipcRenderer.on("update-message", (_event, value) => callback(value)),// 獲取配置getConfig: () => ipcRenderer.invoke("get-config"),// 更新配置updateConfig: (config: Partial<AppConfig>) =>ipcRenderer.invoke("update-config", config),
});

src/ipc.ts

import { ipcMain, BrowserWindow } from "electron";
import { configManager } from './config'
export function setupIPC(mainWindow: BrowserWindow) {ipcMain.handle('get-config', () => {return configManager.get()})

src/main.ts

import { setupIPC } from "./ipc";
setupIPC(mainWindow);

配置頁更新配置

  1. 配置頁深度監聽配置變量,當頁面配置發生改變時,觸發 electron 的 updateConfig 事件,將新配置傳給主進程
  2. 主進程將新配置寫入本地文件

src/views/Settings.vue

深度監聽配置變量,當頁面配置發生改變時,觸發 electron 的 updateConfig 事件,將新配置傳給主進程

// 深度監聽配置變化并自動保存
watch(currentConfig,async (newConfig) => {// 創建一個普通對象來傳遞配置const configToSave = {language: newConfig.language,fontSize: newConfig.fontSize,providerConfigs: JSON.parse(JSON.stringify(newConfig.providerConfigs)),};// 由于 TypeScript 提示 window 上不存在 electronAPI 屬性,我們可以使用類型斷言來解決這個問題await (window as any).electronAPI.updateConfig(configToSave);// 更新界面語言locale.value = newConfig.language;},{ deep: true }
);

src/preload.ts

contextBridge.exposeInMainWorld("electronAPI", {startChat: (data: CreateChatProps) => ipcRenderer.send("start-chat", data),onUpdateMessage: (callback: OnUpdatedCallback) =>ipcRenderer.on("update-message", (_event, value) => callback(value)),// 獲取配置getConfig: () => ipcRenderer.invoke("get-config"),// 更新配置updateConfig: (config: Partial<AppConfig>) =>ipcRenderer.invoke("update-config", config),
});

src/ipc.ts

import { ipcMain, BrowserWindow } from "electron";
import { configManager } from './config'
export function setupIPC(mainWindow: BrowserWindow) {ipcMain.handle("update-config", async (event, newConfig) => {const updatedConfig = await configManager.update(newConfig);return updatedConfig;});

src/config.ts

完整代碼見上文,此處僅截取更新配置的代碼

  async update(newConfig: Partial<AppConfig>) {config = { ...config, ...newConfig };await this.save();return config;},async save() {await fs.writeFile(configPath, JSON.stringify(config, null, 2));return config;},

配置頁完整代碼

src/views/Settings.vue

<template><div class="w-[80%] mx-auto p-8"><h1 class="text-2xl font-bold mb-8">{{ t("settings.title") }}</h1><TabsRoot v-model="activeTab" class="w-full"><TabsList class="flex border-b border-gray-200 mb-6"><TabsTriggervalue="general"class="px-4 py-2 -mb-[1px] text-sm font-medium text-gray-600 hover:text-gray-800 data-[state=active]:text-green-600 data-[state=active]:border-b-2 data-[state=active]:border-green-600">{{ t("settings.general") }}</TabsTrigger><TabsTriggervalue="models"class="px-4 py-2 -mb-[1px] text-sm font-medium text-gray-600 hover:text-gray-800 data-[state=active]:text-green-600 data-[state=active]:border-b-2 data-[state=active]:border-green-600">{{ t("settings.models") }}</TabsTrigger></TabsList><TabsContent value="general" class="space-y-6 max-w-[500px]"><!-- Language Setting --><div class="setting-item flex items-center gap-8"><label class="text-sm font-medium text-gray-700 w-24">{{ t("settings.language") }}</label><SelectRoot v-model="currentConfig.language" class="w-[160px]"><SelectTriggerclass="inline-flex items-center justify-between rounded-md px-3 py-2 text-sm gap-1 bg-white border border-gray-300"><SelectValue :placeholder="t('settings.selectLanguage')" /><SelectIcon><Icon icon="radix-icons:chevron-down" /></SelectIcon></SelectTrigger><SelectPortal><SelectContent class="bg-white rounded-md shadow-lg border"><SelectViewport class="p-2"><SelectGroup><SelectItemvalue="zh"class="relative flex items-center px-8 py-2 text-sm text-gray-700 rounded-md cursor-default hover:bg-gray-100"><SelectItemText>{{ t("common.chinese") }}</SelectItemText><SelectItemIndicatorclass="absolute left-2 inline-flex items-center"><Icon icon="radix-icons:check" /></SelectItemIndicator></SelectItem><SelectItemvalue="en"class="relative flex items-center px-8 py-2 text-sm text-gray-700 rounded-md cursor-default hover:bg-gray-100"><SelectItemText>{{ t("common.english") }}</SelectItemText><SelectItemIndicatorclass="absolute left-2 inline-flex items-center"><Icon icon="radix-icons:check" /></SelectItemIndicator></SelectItem></SelectGroup></SelectViewport></SelectContent></SelectPortal></SelectRoot></div><!-- Font Size Setting --><div class="setting-item flex items-center gap-8"><label class="text-sm font-medium text-gray-700 w-24">{{ t("settings.fontSize") }}</label><NumberFieldRootv-model="currentConfig.fontSize"class="inline-flex w-[100px]"><NumberFieldDecrementclass="px-2 border border-r-0 border-gray-300 rounded-l-md hover:bg-gray-100 focus:outline-none"><Icon icon="radix-icons:minus" /></NumberFieldDecrement><NumberFieldInputclass="w-10 px-2 py-2 border border-gray-300 focus:outline-none focus:ring-1 focus:ring-green-500 text-center":min="12":max="20"/><NumberFieldIncrementclass="px-2 border border-l-0 border-gray-300 rounded-r-md hover:bg-gray-100 focus:outline-none"><Icon icon="radix-icons:plus" /></NumberFieldIncrement></NumberFieldRoot></div></TabsContent><TabsContent value="models" class="space-y-4"><AccordionRoot type="single" collapsible><AccordionItemv-for="provider in providers":key="provider.id":value="provider.name"class="border rounded-lg mb-2"><AccordionTriggerclass="flex items-center justify-between w-full p-4 text-left"><div class="flex items-center gap-2"><img:src="provider.avatar":alt="provider.name"class="w-6 h-6 rounded"/><span class="font-medium">{{ provider.title }}</span></div><Iconicon="radix-icons:chevron-down"class="transform transition-transform duration-200 ease-in-out data-[state=open]:rotate-180"/></AccordionTrigger><AccordionContent class="p-4 pt-0"><div class="space-y-4"><divv-for="config in getProviderConfig(provider.name)":key="config.key"class="flex items-center gap-4"><label class="text-sm font-medium text-gray-700 w-24">{{config.label}}</label><input:type="config.type":placeholder="config.placeholder":required="config.required":value="config.value"@input="(e) => updateProviderConfig(provider.name, config.key, (e.target as HTMLInputElement).value)"class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-green-500"/></div></div></AccordionContent></AccordionItem></AccordionRoot></TabsContent></TabsRoot></div>
</template><script setup lang="ts">
import { reactive, onMounted, watch, ref, computed } from "vue";
import { Icon } from "@iconify/vue";
import { useI18n } from "vue-i18n";
import { AppConfig } from "../types";
import { useProviderStore } from "../stores/provider";
import { providerConfigs, ProviderConfigItem } from "../config/providerConfig";
import {SelectContent,SelectGroup,SelectIcon,SelectItem,SelectItemIndicator,SelectItemText,SelectPortal,SelectRoot,SelectTrigger,SelectValue,SelectViewport,NumberFieldRoot,NumberFieldInput,NumberFieldIncrement,NumberFieldDecrement,TabsRoot,TabsList,TabsTrigger,TabsContent,AccordionRoot,AccordionItem,AccordionTrigger,AccordionContent,
} from "radix-vue";const { t, locale } = useI18n();
const activeTab = ref("general");
const providerStore = useProviderStore();
const providers = computed(() => providerStore.items);const currentConfig = reactive<AppConfig>({language: "zh",fontSize: 14,providerConfigs: {},
});onMounted(async () => {const config = await (window as any).electronAPI.getConfig();Object.assign(currentConfig, config);
});// 深度監聽配置變化并自動保存
watch(currentConfig,async (newConfig) => {// 創建一個普通對象來傳遞配置const configToSave = {language: newConfig.language,fontSize: newConfig.fontSize,providerConfigs: JSON.parse(JSON.stringify(newConfig.providerConfigs)),};// 由于 TypeScript 提示 window 上不存在 electronAPI 屬性,我們可以使用類型斷言來解決這個問題await (window as any).electronAPI.updateConfig(configToSave);// 更新界面語言locale.value = newConfig.language;},{ deep: true }
);// 獲取provider對應的配置項
const getProviderConfig = (providerName: string): ProviderConfigItem[] => {const configs = providerConfigs[providerName] || [];// 確保配置值被初始化if (!currentConfig.providerConfigs[providerName]) {currentConfig.providerConfigs[providerName] = {};}return configs.map((config) => ({...config,value:currentConfig.providerConfigs[providerName][config.key] || config.value,}));
};// 更新provider配置值
const updateProviderConfig = (providerName: string,key: string,value: string
) => {if (!currentConfig.providerConfigs[providerName]) {currentConfig.providerConfigs[providerName] = {};}currentConfig.providerConfigs[providerName][key] = value;
};
</script>

src/config/providerConfig.ts

export interface ProviderConfigItem {key: string;label: string;value: string;type: 'text' | 'password' | 'number';required?: boolean;placeholder?: string;
}// 百度文心一言配置
export const qianfanConfig: ProviderConfigItem[] = [{key: 'accessKey',label: 'Access Key',value: '',type: 'text',required: true,placeholder: '請輸入Access Key'},{key: 'secretKey',label: 'Secret Key',value: '',type: 'password',required: true,placeholder: '請輸入Secret Key'}
];// API Key + Base URL 通用配置模板
export const apiKeyBaseUrlConfig: ProviderConfigItem[] = [{key: 'apiKey',label: 'API Key',value: '',type: 'password',required: true,placeholder: '請輸入API Key'},{key: 'baseUrl',label: 'Base URL',value: '',type: 'text',required: false,placeholder: '請輸入API基礎URL'}
];// 所有Provider的配置映射
export const providerConfigs: Record<string, ProviderConfigItem[]> = {qianfan: qianfanConfig,aliyun: apiKeyBaseUrlConfig,deepseek: apiKeyBaseUrlConfig,openai: apiKeyBaseUrlConfig
}; 

src/stores/provider.ts

import { defineStore } from 'pinia'
import { db } from '../db'
import { ProviderProps } from '../types'export interface ProviderStore {items: ProviderProps[]
}export const useProviderStore = defineStore('provider', {state: (): ProviderStore => {return {items: []}},actions: {async fetchProviders() {const items = await db.providers.toArray()this.items = items}},getters: {getProviderById: (state) => (id: number) => {return state.items.find(item => item.id === id)}}
})

src/db.ts

import Dexie, { type EntityTable } from "dexie";
import { ConversationProps, ProviderProps } from "./types";
import { providers } from "./initData";export const db = new Dexie("AI_chatDatabase") as Dexie & {conversations: EntityTable<ConversationProps, "id">;providers: EntityTable<ProviderProps, "id">;
};db.version(1).stores({// 主鍵為id,且自增// 新增updatedAt字段,用于排序conversations: "++id, updatedAt",providers: "++id, name",
});export const initProviders = async () => {const count = await db.providers.count();if (count === 0) {db.providers.bulkAdd(providers);}
};

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/903660.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/903660.shtml
英文地址,請注明出處:http://en.pswp.cn/news/903660.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

RPG4.設置角色輸入

這一篇是進行玩家移動和視角移動的介紹。 1.在玩家內進行移動覆寫 virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override; 2.創建增強輸入資產的變量創建 UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category "CharacterD…

[實戰] Petalinux驅動開發以及代碼框架解讀

目錄 Petalinux驅動開發以及代碼框架解讀一、引言二、步驟2.1 創建PetaLinux工程2.2 配置硬件描述文件2.3 設備樹配置2.4 建立驅動框架2.5 編輯 .bb 文件2.6 編寫驅動文件2.7 編寫 Makefile2.8 驗證配方配置2.9 集成驅動到 RootFS2.10 全系統編譯與部署2.11 啟動驗證 三、框架解…

[特殊字符] 開發工作高內存占用場景下,Windows 內存壓縮機制是否應該啟用?實測分析與優化建議

在日常開發中&#xff0c;我們往往需要同時運行多個高占用內存的工具&#xff0c;例如&#xff1a; IntelliJ IDEA VMware 虛擬機 多個 Java 后端程序 這些應用程序非常“吃內存”&#xff0c;輕松就能把 16GB、甚至 24GB 的物理內存用滿。那么&#xff0c;Windows 的“內存…

嵌入式學習筆記 - HAL_xxx_MspInit(xxx);函數

使用cubeMX生成的HAL庫函數中&#xff0c;所有外設的初始化函數HAL_xxx_Init(&xxxHandle)中都存在有此調用函數HAL_xxx_MspInit(xxx)&#xff0c;此調用函數其實是對各外設模塊比如UART&#xff0c;I2C等控制器使用的的底層硬件進行初始化&#xff0c;包括時鐘&#xff0c;…

Nginx — http、server、location模塊下配置相同策略優先級問題

一、配置優先級簡述 在 Nginx 中&#xff0c;http、server、location 模塊下配置相同策略時是存在優先級的&#xff0c;一般遵循 “范圍越小&#xff0c;優先級越高” 的原則&#xff0c;下面為你詳細介紹&#xff1a; 1. 配置繼承關系 http 塊&#xff1a;作為全局配置塊&…

WPF之TextBlock控件詳解

文章目錄 1. TextBlock控件介紹2. TextBlock的基本用法2.1 基本語法2.2 在代碼中創建TextBlock 3. TextBlock的常用屬性3.1 文本內容相關屬性3.2 字體相關屬性3.3 外觀相關屬性3.4 布局相關屬性 4. TextBlock文本格式化4.1 使用Run元素進行內聯格式化4.2 其他內聯元素 5. 處理長…

華為云loT物聯網介紹與使用

&#x1f310; 華為云 IoT 物聯網平臺詳解&#xff1a;構建萬物互聯的智能底座 隨著萬物互聯時代的到來&#xff0c;物聯網&#xff08;IoT&#xff09;已成為推動數字化轉型的關鍵技術之一。華為云 IoT 平臺&#xff08;IoT Device Access&#xff09;作為華為云的核心服務之…

AnimateCC教學:形狀補間動畫的代碼實現

核心代碼: var shape; var animationProps = {width: 50,height: 50,cornerRadius: 0,color: "#00FF00" }; function init() { shape = new createjs.Shape();shape.x = 200;shape.y = 150;stage.addChild(shape);// 初始繪制updateShape();// 設置補間動畫createTw…

Android學習總結之Retrofit篇

1. 注解原理概述 在 Java 里&#xff0c;注解是一種元數據&#xff0c;它為代碼提供額外信息但不影響程序的實際邏輯。注解可以在類、方法、字段等元素上使用&#xff0c;并且能在編譯時、運行時通過反射機制被讀取。Retrofit 充分利用了 Java 注解機制&#xff0c;通過自定義…

windows11 編譯 protobuf-3.21.12 c++

下載 protobuf 包&#xff0c;本文使用 3.21.12 版本&#xff0c;Gitub下載鏈接&#xff1a; Github官網 , 網盤下載&#xff1a; 網盤 如果電腦環境沒有安裝 cmake 則需要安裝&#xff0c;本文測試使用 cmake-3.25.1 版本&#xff0c; 下載地址&#xff1a;[camke-3.25.1] (…

Java繼承中super的使用方法

super 關鍵字在 Java 中用于訪問父類的成員&#xff08;包括字段、方法和構造函數&#xff09;。當你在子類中調用父類的方法或訪問父類的成員變量時&#xff0c;super 是必不可少的工具。 &#x1f511; super 的基本用法 1. 調用父類的構造方法 在子類的構造方法中&#x…

網絡安全之淺析Java反序列化題目

前言 這段時間做了幾道Java反序列化題目&#xff0c;發現很多題目都是類似的&#xff0c;并且可以通過一些非預期gadget打進去&#xff0c;就打算總結一下常見的題目類型以及各種解法&#xff0c;并提煉出一般性的思維方法。 正文 分析入口點 拿到題目&#xff0c;有附件最…

動態規劃問題,下降路徑最小和(dp初始化問題,狀態壓縮),單詞拆分(回溯法+剪枝+記憶化),substr函數

下降路徑最小和 題目鏈接&#xff1a; 931. 下降路徑最小和 - 力扣&#xff08;LeetCode&#xff09; 題目描述&#xff1a; 給你一個 n x n 的 方形 整數數組 matrix &#xff0c;請你找出并返回通過 matrix 的下降路徑 的 最小和 。 下降路徑 可以從第一行中的任何元素開…

大數據治理自動化與智能化實踐指南:架構、工具與實戰方案(含代碼)

??個人主頁??:一ge科研小菜雞-CSDN博客 ????期待您的關注 ???? 一、引言:從人治到機治,數據治理正在進化 隨著數據體量持續膨脹、數據場景復雜化,傳統依賴人工規則的大數據治理方式已難以為繼。企業在治理過程中面臨: 數據質量問題激增,人工檢測成本高 元數…

Golang - 實現文件管理服務器

先看效果&#xff1a; 代碼如下&#xff1a; package mainimport ("fmt""html/template""log""net/http""os""path/filepath""strings" )// 配置根目錄&#xff08;根據需求修改&#xff09; //var ba…

Linux-04-用戶管理命令

一、useradd添加新用戶: 基本語法: useradd 用戶名:添加新用戶 useradd -g 組名 用戶:添加新用戶到某個組二、passwd設置用戶密碼: 基本語法: passwd 用戶名:設置用戶名密碼 三、id查看用戶是否存在: 基本語法: id 用戶名 四、su切換用戶: 基本語法: su 用戶名稱:切換用…

Ollama 安裝 QWen3 及配置外網訪問指南

一、Ollama 安裝 QWen3 安裝步驟 首先嘗試運行 QWen3 模型&#xff1a; ollama run qwen3 如果遇到版本不兼容錯誤&#xff08;Error 412&#xff09;&#xff0c;表示需要升級 Ollama&#xff1a; curl -fsSL https://ollama.com/install.sh | sh 驗證版本&#xff1a; o…

高性能架構設計-數據庫(讀寫分離)

一、高性能數據庫簡介 1.高性能數據庫方式 讀寫分離&#xff1a;將訪問壓力分散到集群中的多個節點&#xff0c;沒有分散存儲壓力 分庫分表&#xff1a;既可以分散訪問壓力&#xff0c;又可以分散存儲壓力 2.為啥不用表分區 如果SQL不走分區鍵&#xff0c;很容易出現全表鎖…

【Hive入門】Hive性能優化:執行計劃分析EXPLAIN命令的使用

目錄 1 EXPLAIN命令簡介 1.1 什么是EXPLAIN命令&#xff1f; 1.2 EXPLAIN命令的語法 2 解讀執行計劃中的MapReduce階段 2.1 執行計劃的結構 2.2 Hive查詢執行流程 2.3 MapReduce階段的詳細解讀 3 識別性能瓶頸 3.1 數據傾斜 3.2 Shuffle開銷 3.3 性能瓶頸識別與優化 4 總結 在大…

開源模型應用落地-qwen模型小試-Qwen3-8B-快速體驗(一)

一、前言 阿里云最新推出的 Qwen3-8B 大語言模型,作為國內首個集成“快思考”與“慢思考”能力的混合推理模型,憑借其 80 億參數規模及 128K 超長上下文支持,正在重塑 AI 應用邊界。該模型既可通過輕量化“快思考”實現低算力秒級響應,也能在復雜任務中激活深度推理模式,以…