• Electron-ChatGPT桌面端ChatGPT实例|electron25+vue3聊天AI模板EXE


    基于electron25+vite4+vue3仿制chatgpt客户端聊天模板ElectronChatGPT

    electron-chatgpt 使用最新桌面端技术Electron25.x结合Vite4.x全家桶技术开发跨端模仿ChatGPT智能聊天程序模板。支持经典+分栏两种布局、暗黑+明亮主题模式,集成electron封装多窗口及通讯功能。

    技术栈

    • 编码工具:vscode
    • 框架技术:electron25+vite4+vue3+pinia2
    • 组件库:veplus (基于vue3自定义组件库)
    • 打包工具:electron-builder^23.6.0
    • 调试工具:electron-devtools-installer^3.2.0
    • 代码高亮:highlight.js^11.7.0
    • markdown组件:vue3-markdown-it
    • 本地缓存:pinia-plugin-persistedstate^3.1.0
    • electron结合vite插件:vite-plugin-electron^0.11.2

    项目结构

    基于electron最新版本融合vite4.x技术搭建模仿chatgpt桌面端程序。

    如果对electron+vite4创建跨端应用及多开窗口感兴趣,可以去看看之前的这两篇分享文章。

    https://www.cnblogs.com/xiaoyan2017/p/17436076.html

    https://www.cnblogs.com/xiaoyan2017/p/17442502.html

    随着electron快速迭代更新,加上vite极速编译,二者配合创建的应用运行速度超快。

    Vue3桌面UI组件库

    考虑到项目比较轻量级,所以采用自研vue3组件库ve-plus

    关于veplus组件库这里不作过多介绍,之前有过一篇分享文章,大家可以去看看。

    https://www.cnblogs.com/xiaoyan2017/p/17170454.html

    项目布局

    项目整体大致分为顶部导航工具栏+左侧会话记录/操作链接+右侧会话区/编辑框等模块。

    复制代码
    <template>
        <div class="vegpt__layout flexbox flex-col">
            
            <Toolbar />
            
            <div class="ve__layout-body flex1 flexbox">
                
                <div class="ve__layout-menus flexbox" :class="{'hidden': store.config.collapse}">
                    <aside class="ve__layout-aside flexbox flex-col">
                        <ChatNew />
                        <Scrollbar class="flex1" autohide size="4" gap="1">
                            <ChatList />
                        Scrollbar>
                        <ExtraLink />
                        <Collapse />
                    aside>
                div>
    
                
                <div class="ve__layout-main flex1 flexbox flex-col">
                    <Main />
                div>
            div>
        div>
    template>
    复制代码

    Electron主进程入口

    根目录下新建 electron-main.js 作为主进程入口文件。

    复制代码
    /**
     * 主进程入口
     * @author YXY
     */
    
    const { app, BrowserWindow } = require('electron')
    
    const MultiWindow = require('./src/multiwindow')
    
    // 屏蔽安全警告
    // ectron Security Warning (Insecure Content-Security-Policy)
    process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
    
    const createWindow = () => {
        let win = new MultiWindow()
        win.createWin({isMainWin: true})
    }
    
    app.whenReady().then(() => {
        createWindow()
        app.on('activate', () => {
            if(BrowserWindow.getAllWindows().length === 0) createWindow()
        })
    })
    
    app.on('window-all-closed', () => {
        if(process.platform !== 'darwin') app.quit()
    })
    复制代码

    使用electron的vite插件,在vite.config.js中配置入口。

    复制代码
    import { defineConfig, loadEnv } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import electron from 'vite-plugin-electron'
    import { resolve } from 'path'
    import { parseEnv } from './src/utils/env'
    
    export default defineConfig(({ command, mode }) => {
      const viteEnv = loadEnv(mode, process.cwd())
      const env = parseEnv(viteEnv)
    
      return {
        plugins: [
          vue(),
          electron({
            // 主进程入口文件
            entry: 'electron-main.js'
          })
        ],
        
        /*构建选项*/
        build: {
          /* minify: 'esbuild', // 打包方式 esbuild(打包快)|terser
          chunkSizeWarningLimit: 2000, // 打包大小警告
          rollupOptions: {
              output: {
                  chunkFileNames: 'assets/js/[name]-[hash].js',
                  entryFileNames: 'assets/js/[name]-[hash].js',
                  assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
              }
          } */
          
          // 如果打包方式是terser,则配置如下
          /* minify: "terser",
          terserOptions: {
            compress: {
              // 去掉所有console和debugger
              // drop_console: true,
              // drop_debugger: true,
    
              drop_console: command !== 'serve',
              drop_debugger: command !== 'serve',
              //pure_funcs:['console.log'] // 移除console.log
            }
          } */
        },
        esbuild: {
          // 打包去除 console.log 和 debugger
          drop: env.VITE_DROP_CONSOLE && command === 'build' ? ["console", "debugger"] : []
        },
    
        /*开发服务器选项*/
        server: {
          // 端口
          port: env.VITE_PORT,
          // ...
        },
    
        resolve: {
          // 设置别名
          alias: {
            '@': resolve(__dirname, 'src'),
            '@assets': resolve(__dirname, 'src/assets'),
            '@components': resolve(__dirname, 'src/components'),
            '@views': resolve(__dirname, 'src/views')
          }
        }
      }
    })
    复制代码

    需要注意:由于目前Electron 尚未支持 "type": "module",需要在package.json中去掉,并且配置 "main": "electron-main.js", 入口。

    Electron自定义无边框窗口工具栏

    创建窗口的时候配置 frame: false 参数,创建的窗口则没有系统顶部导航栏及边框。拖拽区域/最大化/最小化及关闭按钮均需要自定义操作。

    通过设置css3属性 -webkit-app-region: drag ,则可对自定义区域进行拖拽操作,设置后按钮/链接点击则会失效,这时通过对按钮或链接设置-webkit-app-region: no-drag就可恢复事件响应。

    不过设置-webkit-app-region: drag,点击鼠标右键,会出现上图系统菜单,经过一番调试,windows下可以暂时通过如下方法屏蔽右键菜单。

    复制代码
    // 屏蔽系统右键菜单
    win.hookWindowMessage(278, () => {
        win.setEnabled(false)
        setTimeout(() => {
            win.setEnabled(true)
        }, 100)
    
        return true
    })
    复制代码

    components/titlebar目录自定义工具栏条。

    control.vue自定义最大化/最小化/关闭按钮

    复制代码
    <template>
        <div class="vegpt__control ve__nodrag">
            <div class="vegpt__control-btns" :style="{'color': color}">
                <slot />
                <div v-if="isTrue(minimizable)" class="btn win-btn win-min" @click="handleMin"><i class="iconfont ve-icon-minimize">i>div>
                <div v-if="isTrue(maximizable) && winCfg.window.resizable" class="btn win-btn win-maxmin" @click="handleRestore">
                    <i class="iconfont" :class="isMaximized ? 've-icon-maxrestore' : 've-icon-maximize'">i>
                div>
                <div v-if="isTrue(closable)" class="btn win-btn win-close" @click="handleQuit"><i class="iconfont ve-icon-close">i>div>
            div>
        div>
    template>
    复制代码
    复制代码
    
    
    
    
    复制代码

    在 index.vue 中引入 control.vue 操作按钮,并支持自定义左侧、标题等功能。

    复制代码
    <template>
        <div class="vegpt__titlebar" :class="{'fixed': isTrue(fixed), 'transparent fixed': isTrue(transparent)}">
            <div class="vegpt__titlebar-wrapper flexbox flex-alignc ve__drag" :style="{'background': bgcolor, 'color': color, 'z-index': zIndex}">
                <slot name="left">
                    <img src="/logo.png" height="20" style="margin-left: 10px;" />
                slot>
                <div class="vegpt__titlebar-title" :class="{'center': isTrue(center)}">
                    <slot name="title">{{ title || winCfg.window.title || env.VITE_APPTITLE }}slot>
                div>
    
                
                <Control :minimizable="minimizable" :maximizable="maximizable" :closable="closable">
                    <slot name="btn" />
                Control>
            div>
        div>
    template>
    复制代码

    Electron创建系统托盘图标

    复制代码
    // 创建系统托盘图标
    createTray() {
        console.log('——+——+——Start Create Tray!')
        console.log(__dirname)
        console.log(join(process.env.ROOT, 'resource/tray.ico'))
        
        const trayMenu = Menu.buildFromTemplate([
            {
                label: '打开主界面',
                icon: join(process.env.ROOT, 'resource/home.png'),
                click: () => {
                    try {
                        for(let i in this.group) {
                            let win = this.getWin(i)
                            if(!win) return
                            // 是否主窗口
                            if(this.group[i].isMainWin) {
                                if(win.isMinimized()) win.restore()
                                win.show()
                            }
                        }
                    } catch (error) {
                        console.log(error)
                    }
                }
            },
            {
                label: '设置中心',
                icon: join(process.env.ROOT, 'resource/setting.png'),
                click: () => {
                    for(let i in this.group) {
                        let win = this.getWin(i)
                        if(win) win.webContents.send('win__ipcData', { type: 'CREATE_WIN_SETTING', value: null })
                    }
                },
            },
            {
                label: '锁屏',
                icon: join(process.env.ROOT, 'resource/lock.png'),
                click: () => null,
            },
            {
                label: '关闭托盘闪烁',
                click: () => {
                    this.flashTray(false)
                }
            },
            {type: 'separator'},
            /* {
                label: '重启',
                click: () => {
                    // app.relaunch({ args: process.argv.slice(1).concat(['--relaunch']) })
                    // app.exit(0)
                }
            }, */
            {
                label: '关于',
                click: () => {
                    for(let i in this.group) {
                        let win = this.getWin(i)
                        if(win) win.webContents.send('win__ipcData', { type: 'CREATE_WIN_ABOUT', value: null })
                    }
                }
            },
            {
                label: '关闭应用并退出',
                icon: join(process.env.ROOT, 'resource/quit.png'),
                click: () => {
                    dialog.showMessageBox(this.main, {
                        title: '询问',
                        message: '确定要退出应用程序吗?',
                        buttons: ['取消', '最小化托盘', '退出应用'],
                        type: 'error',
                        noLink: false,  // true传统按钮样式  false链接样式
                        cancelId: 0
                    }).then(res => {
                        console.log(res)
    
                        const index = res.response
                        if(index == 0) {
                            console.log('取消')
                        }if(index == 1) {
                            console.log('最小化托盘')
                            for(let i in this.group) {
                                let win = this.getWin(i)
                                if(win) win.hide()
                            }
                        }else if(index == 2) {
                            console.log('退出应用')
    
                            try {
                                for(let i in this.group) {
                                    let win = this.getWin(i)
                                    if(win) win.webContents.send('win__ipcData', { type: 'WIN_LOGOUT', value: null })
                                }
                                // app.quit 和 app.exit(0) 都可退出应用。
                                // 前者可以被打断并触发一些事件,而后者将强制应用程序退出而不触发任何事件或允许应用程序取消操作。
                                app.quit()
                            } catch (error) {
                                console.log(error)
                            }
                        }
                    })
                }
            }
        ])
        this.tray = new Tray(this.trayIco1)
        this.tray.setContextMenu(trayMenu)
        this.tray.setToolTip(app.name)
        this.tray.on('double-click', () => {
            console.log('double clicked')
        })
    
        // 开启托盘闪烁
        // this.flashTray(true)
    }
    复制代码

    托盘图标、右键菜单图标及打包图标均在resource目录下。

    Electron打包脚本electron-builder

    在根目录新建一个electron打包配置文件electron-builder.json。

    复制代码
    {
        "productName": "Electron-ChatGPT",
        "appId": "com.yxy.electron-chatgpt-vue3",
        "copyright": "Copyright © 2023-present Andy",
        "compression": "maximum",
        "asar": true,
        "directories": {
            "output": "release/${version}"
        },
        "nsis": {
            "oneClick": false,
            "allowToChangeInstallationDirectory": true,
            "perMachine": true,
            "deleteAppDataOnUninstall": true,
            "createDesktopShortcut": true,
            "createStartMenuShortcut": true,
            "shortcutName": "ElectronVite4Vue3"
        },
        "win": {
            "icon": "./resource/shortcut.ico",
            "artifactName": "${productName}-v${version}-${platform}-${arch}-setup.${ext}",
            "target": [
                {
                    "target": "nsis",
                    "arch": ["ia32"]
                }
            ]
        },
        "mac": {
            "icon": "./resource/shortcut.icns",
            "artifactName": "${productName}-v${version}-${platform}-${arch}-setup.${ext}"
        },
        "linux": {
            "icon": "./resource",
            "artifactName": "${productName}-v${version}-${platform}-${arch}-setup.${ext}"
        }
    }
    复制代码

    Electron主渲染进程通讯传值

    由于electron主渲染进程一般都是单窗口之间进行传值。如果需要在多个窗口间传值,如切换主题功能,则需要在渲染进程发送请求,主进程监听后再发送请求给渲染进程(App.vue中监听)。

    复制代码
    <div
        class="toolbar__item"
        :title="`切换 暗黑/明亮 模式(当前 ${appState.config.isDark ? '暗黑' : '明亮'}模式)`"
        @click="changeMode"
    >
        <Icon :name="appState.config.isDark ? 've-icon-sunny' : 've-icon-yewan'" />
    div>
    
    // 主题切换
    const changeMode = () => {
        appState.config.isDark = !appState.config.isDark
        ipcRenderer.send('win__postData', appState.config.isDark)
    }
    复制代码

    在主进程中使用ipcMain.on监听。

    // 主/渲染进程传参
    ipcMain.on('win__postData', (event, args) => {
        mainWin.webContents.send('win__postData', args)
    })

    然后在渲染进程App.vue页面监听并处理通讯传值。

    复制代码
    /**
     * 接收主进程发送的事件
     */
    ipcRenderer.on('win__postData', (e, data) => {
        console.log('——+——+——receive multiwin data:', data)
    
        switch(data.type) {
            // 退出登录
            case 'WIN_LOGOUT':
                appState.$reset()
                break;
            // 布局切换
            case 'CHANGE_LAYOUT':
                appState.config.layout = data.value
                break;
            // 切换主题
            case 'CHANGE_MODE':
                appState.config.isDark = data.value
                appState.changeDark()
                break;
            // 侧边栏收缩
            case 'CHANGE_COLLAPSE':
                appState.config.collapse = data.value
                break;
          }
    })
    复制代码

    这样就能简单实现多窗口传值了。如果大家有其他方法,欢迎一起交流学习哈~

    Ok,基于electron25+vue3开发桌面端仿chatgpt聊天实例就先分享到这里,希望对大家有所帮助😎

    最后附上一个Vue3+Tauri跨端聊天项目

    https://www.cnblogs.com/xiaoyan2017/p/16830689.html

     

  • 相关阅读:
    阅读笔记_一本书读懂财报
    【django】Forbidden (CSRF cookie not set.)
    市场上ios签名公司做什么的?
    java(反射机制)
    文件对话框
    【有ISSN、ISBN号!!往届均已完成EI检索】2024年第四届计算机视觉与模式分析国际学术大会(ICCPA 2024)
    java-net-php-python-jspm校园线上零食屋计算机毕业设计程序
    Fast Large-Scale Trajectory Clustering(VLDB2019)
    SpringBoot面试
    go 获取文件类型
  • 原文地址:https://www.cnblogs.com/xiaoyan2017/p/17468074.html