当前位置: 首页 > news >正文

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.xdnf.cn/news/214867.html

相关文章:

  • 【含文档+PPT+源码】基于微信小程序的乡村振兴民宿管理系统
  • BLE技术,如何高效赋能IoT短距无线通信?
  • 【展位预告】正也科技将携营销精细化管理解决方案出席中睿营销论坛
  • 数据库系统概论|第三章:关系数据库标准语言SQL—课程笔记7
  • Unity Audio DSP应用与实现
  • C++多线程与锁机制
  • JavaScript函数声明大比拼
  • yolov8使用
  • 10 基于Gazebo和Rviz实现导航仿真,包括SLAM建图,地图服务,机器人定位,路径规划
  • BIM(建筑信息模型)与GIS(地理信息系统)的融合的技术框架、实现路径与应用场景
  • 【MCP Node.js SDK 全栈进阶指南】高级篇(2):MCP高性能服务优化
  • MCP 协议 ——AI 世界的 “USB-C 接口”:从认知到实践的全面指南
  • 源码角度分析 sync.map
  • Silvaco仿真中victory process的蒙特卡洛(Monte Carlo)离子注入
  • [4-06-09].第10节:自动配置- 分析@SpringBootApplication启动类
  • github使用记录
  • Redis分布式锁使用以及对接支付宝,paypal,strip跨境支付
  • 第十六届蓝桥杯大赛网安组--几道简单题的WP
  • HTTP协议重定向及交互
  • 运放参数汇总
  • mac word接入deepseek
  • LVGL -窗口操作
  • Linux/AndroidOS中进程间的通信线程间的同步 - 管道和FIFO
  • 【C++编程入门】:基本语法
  • Java 多线程基础:Thread 类详解
  • 云数据中心整体规划方案PPT(113页)
  • VIT(ICLR2021)
  • foc控制 - clarke变换和park变换
  • 【后端】【Docker】 Docker 动态代理 取消代理完整脚本合集(Ubuntu)
  • 内网服务器映射到公网上怎么做?网络将内网服务转换到公网上