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

这篇具有很好参考价值的文章主要介绍了Electron-ChatGPT桌面端ChatGPT实例|electron25+vue3聊天AI模板EXE。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

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

技术栈

  • 编码工具: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-ChatGPT桌面端ChatGPT实例|electron25+vue3聊天AI模板EXE

项目结构

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Vue3桌面UI组件库

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

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

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

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

项目布局

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

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

<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 作为主进程入口文件。

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

/**
 * 主进程入口
 * @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自定义无边框窗口工具栏

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

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

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

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

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

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

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

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

    return true
})

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

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

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>
<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>


<script setup>
    import { onMounted, ref } from 'vue'
    import { winCfg, setWin } from '@/multiwindow/actions'
    import { appStore } from '@/pinia/modules/app'
    import { isTrue } from '@/utils'

    const appState = appStore()

    const props = defineProps({
        // 标题颜色
        color: String,

        // 窗口是否可以最小化
        minimizable: { type: [Boolean, String], default: true },
        // 窗口是否可以最大化
        maximizable: { type: [Boolean, String], default: true },
        // 窗口是否可以关闭
        closable: { type: [Boolean, String], default: true }
    })

    // 是否最大化
    let isMaximized = ref(false)

    onMounted(() => {
        window.electronAPI.invoke('win__isMaximized').then(data => {
            console.log(data)
            isMaximized.value = data
        })
        window.electronAPI.receive('win__hasMaximized', (e, data) => {
            console.log(data)
            isMaximized.value = data
        })
    })

    // 最小化
    const handleMin = () => {
        window.electronAPI.send('win__minimize')
    }
    // 最大化/还原
    const handleRestore = () => {
        window.electronAPI.invoke('win__max2min').then(data => {
            console.log(data)
            isMaximized.value = data
        })
    }
    // 关闭窗体
    const handleQuit = () => {
        if(winCfg.window.isMainWin) {
            MessageBox.confirm('应用提示', '是否最小化到托盘, 不退出程序?', {
                type: 'warning',
                cancelText: '最小化至托盘',
                confirmText: '残忍退出',
                confirmType: 'danger',
                width: 300,
                callback: action => {
                    if(action == 'confirm') {
                        appState.$reset()
                        setWin('close')
                    }else if(action == 'cancel') {
                        setWin('hide', winCfg.window.id)
                    }
                }
            })
        }else {
            setWin('close', winCfg.window.id)
        }
    }
</script>

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

在 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创建系统托盘图标

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

// 创建系统托盘图标
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)
}

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

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

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

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中监听)。

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

<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

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

 文章来源地址https://www.toymoban.com/news/detail-476217.html

到了这里,关于Electron-ChatGPT桌面端ChatGPT实例|electron25+vue3聊天AI模板EXE的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Electron + Vue3 + Vite + TS 构建桌面应用

    之前是使用React、Electron、TS和webpack来构建桌面应用的。虽然功能齐全,但是打包等等开发的体验不太理想,总感觉太慢了。作为一个开发者,我们总是希望,执行构建命令后,可以快速打包或者启动本地应用,且通过更少的配置,来完成开发体验。 现在的vite已经得到广泛的

    2024年02月14日
    浏览(41)
  • 基于Electron24+Vite4+Vue3搭建桌面端应用

    一说到创建桌面应用,就不得不提及Electron和Tauri框架。这次给大家主要分享的是基于electron最新版本整合vite4.x构建vue3桌面端应用程序。 之前也有使用vite2+vue3+electronc创建桌面端项目,不过  vue-cli-plugin-electron-builder  脚手架插件构建的项目electron版本只有13.x。如今electron版本

    2024年02月06日
    浏览(38)
  • 使用Electron + Vue3 + TS搭建桌面端应用并可热更新

    以下是必要的技术: Electron 13.0.0 Vue3 + TS Electron-updater Node 16.13.1 Element-plus Less Meansjs 安装Vue-cli(如果未安装): npm install -g @vue/cli 创建Vue3项目: vue create electron-vue3 启动项目: yarn serve 安装Electron: vue add electron-builder 启动项目: yarn electron:serve 如果报错,需要安装ts-loader: yar

    2023年04月26日
    浏览(70)
  • Electron-React18-MacOS桌面管理系统|electron27+react仿mac桌面

    基于 React18+Electron27+ArcoDesign 仿macOS桌面端系统框架 ElectronMacOS 。 electron-react-macOs 基于 electron27.x+vite4+react18+arcoDesign+zustand 等技术构建桌面版仿MacOs框架系统解决方案。支持 中英文/繁体、dark+light主题、桌面多层级路由、多窗口路由页面、动态换肤、Dock悬浮菜单 等功能。 Elec

    2024年02月05日
    浏览(24)
  • Electron桌面应用开发基础

    Electron 是一种基于 Chromium 和 Node.js 的开源框架,可以用于快速构建跨平台的桌面应用程序。与传统的桌面应用程序不同,Electron 应用程序使用 HTML、CSS 和 JavaScript 技术 栈来实现界面设计和业务逻辑,并且具有良好的跨平台性能和扩展性。 跨平台性:Electron 可以在 Windows、M

    2024年02月08日
    浏览(30)
  • electron桌面开发相关注意点

    electron的部署以及配置 如果使用的是pnpm,请先配置一下镜像,否则会安装失败的: 如果是npm,其实也一样的, 不过你也有另外一个选择: 添加 .npmrc文件,内容如下:

    2024年02月21日
    浏览(19)
  • React使用Electron开发桌面端

    React是一个流行的JavaScript库,用于构建Web应用程序。结合Electron框架,可以轻松地将React应用程序打包为桌面应用程序。以下是使用React和Electron开发桌面应用程序的步骤: 1. 安装Electron 首先,你需要安装Electron。在终端中运行以下命令: 2. 创建Electron应用程序 使用Electron提供

    2024年02月09日
    浏览(21)
  • 使用angular和electron 构建桌面应用

    新建一个angular app 修改src/index.html文件内容 将绝对路径改为相对路径,加个点,使electron可以访问到angular文件资源

    2024年02月14日
    浏览(23)
  • 从零搭建vue+electron桌面应用

    1.全局下载electron 2.全局下载vue脚手架 3.创建vue项目(这里用的是vue2版本) 4.安装打包插件 首先进入项目目录 安装打包插件 5.安装electron-builder,安装后可以直接生成主进程的配置文件 6.在vue.config.js中添加以下配置 作完以上步骤之后,会在src根目录生成background.js,这个文件

    2024年02月17日
    浏览(29)
  • vite + electron 构建前端桌面应用程序

    npm yarn 选择是否继续 项目名称 选择框架 选择项目语言 项目构建成功,根据提示进入项目目录,安装依赖 electron 官网:https://www.electronjs.org/zh/docs/latest/ vite-plugin-electron :https://github.com/electron-vite/vite-plugin-electron 安装依赖(electron 安装比较慢,耐心等待即可) 编写 Electron 入

    2024年02月06日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包