头图

2025跨平台实战AI大模型,基于electron35+vite6+arco-design+markdown对接deepseek-v3聊天模型,实现stream流式打字输出效果。

https://www.bilibili.com/video/BV1AB5azmEyE/?aid=114350668911...

Uniapp-DeepSeek跨三端AI助手|uniapp+vue3+deepseek-v3流式ai聊天模板

vue3-webseek网页版AI问答|Vite6+DeepSeek+Arco流式ai聊天打字效果

基于vue3.5+deepseek-chat流式打字ai聊天会话助手
https://segmentfault.com/a/1190000046225125

使用技术

  • 开发工具:vscode
  • 技术框架:vite^6.2.0+vue^3.5.13+vue-router^4.5.0
  • 大模型ai框架:DeepSeek-V3-0324 + OpenAI
  • 跨平台框架:electron^35.1.2
  • 组件库:arco-design^2.57.0 (字节桌面端组件库)
  • 状态插件:pinia^3.0.1
  • 会话缓存:pinia-plugin-persistedstate^4.2.0
  • 高亮插件:highlight.js^11.11.1
  • markdown解析:markdown-it

功能特性

  1. 基于跨平台Electron35框架,接入DeepSeek-V3,性能更丝滑
  2. 支持各种代码高亮效果、易于展示分享代码片段
  3. 支持上下文多轮对话、提示词生成图片及预览功能
  4. 支持多窗口、亮色+暗黑主题
  5. 支持新窗口打开会话里面的链接
  6. 采用arco-design组件库,风格一致性

项目结构框架

electron35-deepseek客户端ai项目已经发布到我的原创作品小铺。
electron35+deepseek+vite6桌面端AI流式聊天对话EXE

入口配置main.js

import { createApp } from 'vue'
import './style.scss'
import App from './App.vue'

import { launchApp } from '@/windows/actions'

// 引入路由/状态配置
import Router from './router'
import Pinia from './pinia'

// 引入插件配置
import Plugins from './plugins'

launchApp().then(config => {
  if(config) {// 全局存储窗口配置
    window.config = config
  }

  // 初始化app实例
  createApp(App)
  .use(Router)
  .use(Pinia)
  .use(Plugins)
  .mount('#app')
})

环境变量.env配置

# 项目名称
VITE_APPNAME = 'Electron35-DeepSeek'

# 运行端口
VITE_PORT = 3003

# 是否加载调试工具devtools
VITE_DEVTOOLS = true

# 是否打开调试工具devtools
VITE_OPEN_DEVTOOLS = true

# 是否删除生产环境console
VITE_DROP_CONSOLE = true

# DeepSeek API配置
VITE_DEEPSEEK_API_KEY = 替换为你的 API Key
VITE_DEEPSEEK_BASE_URL = https://api.deepseek.com

项目公共模板

7cebf11f836e3f0f72eed833c8b2deab_1289798-20250417114129399-1818820098.png

62e11451ea556e5c40e4016b47a42a94_1289798-20250417113959089-1884145640.png

<script setup>
  import Titlebar from '@/layouts/components/titlebar/index.vue'
  import Sidebar from '@/layouts/components/sidebar/index.vue'
</script>

<template>
  <div class="vu__container">
    <div class="vu__layout flexbox flex-col">
      <!-- 导航栏 -->
      <Titlebar />

      <div class="vu__layout-body flex1 flexbox">
        <!-- 侧边 -->
        <Sidebar />

        <!-- 对话区 -->
        <div class="vu__layout-main flex1">
          <router-view v-slot="{ Component, route }">
            <keep-alive>
              <component :is="Component" :key="route.path" />
            </keep-alive>
          </router-view>
        </div>
      </div>
    </div>
  </div>
</template>

7167e3ec680bf32c6f63b832652d6210_1289798-20250417114904734-2086862290.png

9b4a871fce151925148906abc14d55f5_1289798-20250417114836713-1122021252.png

路由管理

a2a0c172ab384344461cb8798756dd06_1289798-20250417115250956-319960391.png

import { createRouter, createWebHashHistory } from 'vue-router'
import { authState } from '@/pinia/modules/auth'

import Layout from '@/layouts/index.vue'

import { loginWindow } from '@/windows/actions'

// 批量导入路由
const modules = import.meta.glob('./modules/*.js', { eager: true })
const patchRouters = Object.keys(modules).map(key => modules[key].default).flat()

/**
 * meta配置
 * @param meta.requireAuth 需登录验证页面
 */
const routes = [
  ...patchRouters,
  // 错误模块
  {
    path: '/:pathMatch(.*)*',
    redirect: '/404',
    component: Layout,
    meta: {
      title: '404 error',
    },
    children: [
      {
        path: '404',
        component: () => import('@/views/error/404.vue'),
      }
    ]
  },
]

const router = createRouter({
  history: createWebHashHistory(),
  routes,
})

// 全局路由钩子拦截
router.beforeEach((to, from) => {
  const authstate = authState()
  // 登录验证
  if(to?.meta?.requireAuth && !authstate.authorization) {
    // console.log('你还未登录!')
    loginWindow()
  }
})

router.afterEach(() => {
  // ...
})

router.onError(error => {
  console.warn('[Router Error]', error)
})

export default router

对话编辑器

612605ca7fc4781b3ada2b5f6248dfbe_1289798-20250417120714426-1703055759.png

4423663a543d22f97ee49c1481815617_1289798-20250417121055466-430124469.png

d326d8476c8d076d6f0782e2b4b3c746_1289798-20250417121519001-1651496316.png

<template>
  <div class="v3ai__footbar flexbox flex-col">
    <!-- 技能栏 -->
    <div v-if="skillbar" class="v3ai__skills flexbox flex-alignc">
      ...
    </div>
    <!-- 编辑栏 -->
    <div class="v3ai__inputbox flexbox flex-col">
      <div class="v3ai__editor flexbox">
        ...
      </div>
      <!-- 操作栏 -->
      <div class="v3ai__tools flexbox flex-alignc">
        <div class="option flex1 flexbox">
          <div class="btn" :class="{'active': isDeep}" @click="isDeep =! isDeep"><i class="iconfont ai-deepthink"></i> 深度思考 <span class="fs-12">(R1)</span></div>
          <div class="btn" :class="{'active': isNetwork}" @click="isNetwork =! isNetwork"><i class="iconfont ai-network"></i> 联网</div>
        </div>
        <a-dropdown trigger="hover" :show-arrow="false" position="lb" :content-style="{'min-width': '250px'}">
          <a-button shape="circle"><icon-attachment size="18" /></a-button>
          <template #content>
            <a-dgroup>
              <template #title><div style="margin-bottom: 5px;">从网盘添加</div></template>
              <a-doption value="wx"><icon-more /> 选择网盘文件</a-doption>
            </a-dgroup>
            <a-dgroup>
              <template #title><div style="margin-bottom: 5px;">从本地添加</div></template>
              <a-doption value="wx"><icon-apps /> 上传文件</a-doption>
              <a-dsubmenu trigger="hover" position="rb" :popup-translate="[8, 8]" value="option-1" :content-style="{'min-width': '125px'}">
                <template #default><icon-apps /> 上传代码</template>
                <template #content>
                  <a-doption value="pyq"><icon-apps /> 代码文件</a-doption>
                  <a-doption value="qq"><icon-apps /> 代码文件夹</a-doption>
                  <a-doption value="qq"><icon-apps /> GitHub仓库</a-doption>
                </template>
              </a-dsubmenu>
            </a-dgroup>
          </template>
        </a-dropdown>
        <a-tooltip content="截图提问" position="top" mini>
          <a-button shape="circle"><icon-scissor size="18" @click="handleCut" /></a-button>
        </a-tooltip>
        <a-dropdown :show-arrow="false" position="top" :popup-translate="[-10, -5]" :content-style="{'min-width': '160px'}">
          <a-button shape="circle"><icon-plus size="18" /></a-button>
          <template #content>
            <a-doption value="image"><icon-file-image /> 图片</a-doption>
            <a-doption value="file"><icon-file /> 本地文件</a-doption>
            <a-doption value="pdf"><icon-file-pdf /> PDF文档分析</a-doption>
            <a-doption value="page"><icon-cloud /> 网页总结</a-doption>
          </template>
        </a-dropdown>
        <a-divider direction="vertical" style="margin: 0 7px;" />
        <a-button class="submit" type="primary" shape="circle" :disabled="!editorText?.trim() || sessionstate.loading" @click="handleSubmit">
          <icon-arrow-up v-if="!sessionstate.loading" size="20" />
          <icon-loading v-else size="18" />
        </a-button>
      </div>
    </div>
  </div>
</template>

electron35+vue3对接deepseek

const completion = await openai.chat.completions.create({
  // 单一会话
  messages: [ {role: 'user', content: editorValue} ],
  // 多轮会话
  // messages: props.multiConversation ? historySession.value : [{role: 'user', content: editorValue}],
  model: 'deepseek-chat',
  stream: false, // 非流式输出
  max_tokens: 8192, // 限制一次请求中模型生成 completion 的最大 token 数(默认使用 4096)
  temperature: 0.4, // 严谨采样 越低越严谨(默认1)
})

// 一次性返回非流式内容
console.log(completion.choices[0].message.content)

通过for循环输出一个分片流式。

// 使用数组存储chunk内容,提高拼接效率
let chunks = []
let lastUpdate = 0

// 处理流式输出
for await (const chunk of completion) {
  const content = chunk.choices[0]?.delta?.content || ''
  if(content) {
    chunks.push(content)
    // 限制更新频率:每100ms最多更新一次
    const now = Date.now()
    if (now - lastUpdate > 100) {
      sessionstate.updateSession(botKey, {content: chunks.join('')})
      lastUpdate = now
      // 滚动最底部
      if (sessionstate.reachBottom) {
        props.scrollBottom()
      }
    }
  }
  if(chunk.choices[0]?.finish_reason === 'stop') {
    sessionstate.loading = false
    // 确保最终内容完整更新
    sessionstate.updateSession(botKey, {content: chunks.join(''), loading: false})
    if (sessionstate.reachBottom) {
      props.scrollBottom()
    }
  }
}

基于vue3.5+deepseek-chat流式打字ai聊天会话助手

https://segmentfault.com/a/1190000046225125

原创vue3.5接入deepseek-chat网页版ai流式聊天会话系统

https://segmentfault.com/a/1190000046393396

再附上几个实战项目案例


xiaoyan2017
779 声望332 粉丝

web前端开发爱好者,专注于前端h5、jquery、vue、react、angular等技术研发实战项目案例。