vue3实现ai聊天对话框

各功能部分学习

input输入

  1. 使用@keydown 键盘进行操作,回车和点击一样进行搜索

    @keydown.enter.exact.prevent="handleSend"
    @keydown.enter.shift.exact="newline"

  2. 按钮 loading 加载图标:这里设置 template 插槽

    <el-button
    type="primary"
    :loading="loading"
    @click="handleSend"

    <template #icon>
    <el-icon><Position /></el-icon>
    </template>
    发送
    </el-button>

  3. 职责分离,子组件完成页面搭建,需要用到哪些逻辑,再通过 emit 通信到父组件由父组件完成。

  4. 整个页面涉及到一个多个组件使用的 loading 属性,由 settings 仓库存储。

message对话框

  1. 简单的一行代码使用户消息放在右侧

    &.message-user {
    flex-direction: row-reverse;
    //翻转实现用户布局在右侧
    .message-content {
    align-items: flex-end;
    }
    }

  2. 换行属性white-space: pre-wrap;保留源代码中的空白字符和换行符,否则为white-space: normal;(默认值):合并连续的空白字符,忽略源代码中的换行符,自动换行

settings设置面板

  1. 设置属性都设置在仓库里,用于全局。

  2. 样式命名w-full这总可以通俗易懂的看出是宽度占满。

  3. 如何在 stlye 中修改 elementPlus 原本的样式。

  4. elementPlus 设置暗黑模式

    //App.vue

    <template>
    <router-view />
    </template> <script setup> import { computed } from 'vue' import { useSettingsStore } from './stores/settings'

    const settingsStore = useSettingsStore()
    const isDarkMode = computed(() => settingsStore.isDarkMode)
    </script>

    //dark.scss
    html.dark {
    // Element Plus 暗黑模式变量覆盖
    --el-bg-color: var(--bg-color);
    --el-bg-color-overlay: var(--bg-color-secondary);
    --el-text-color-primary: var(--text-color-primary);
    --el-text-color-regular: var(--text-color-regular);
    --el-border-color: var(--border-color);

    // Element Plus 组件暗黑模式样式覆盖
    .el-input-number {
    --el-input-number-bg-color: var(--bg-color-secondary);
    --el-input-number-text-color: var(--text-color-primary);
    }

    .el-select-dropdown {
    --el-select-dropdown-bg-color: var(--bg-color);
    --el-select-dropdown-text-color: var(--text-color-primary);
    }

    .el-slider {
    --el-slider-main-bg-color: var(--primary-color);
    }
    }
    //main.js
    import './assets/styles/dark.scss'

  5. 使用 scss 设置暗黑模式:

    1. 使用pinia状态管理。
    2. 在设置面板点击切换,触发toggleDarkMode动作(在pinia中设置),切换isDarlMode状态,并更新跟元素的data-theme属性。
    3. 暗黑模式的样式设置在scss变量中
  6. 当刷新后样式会变回白天模式,但是这时候settings中选中的还是黑夜模式,这是为什么?

答:虽然设置存储在了 localStorage 中,但是在页面刷新后没有正确地初始化深色模式的样式。我们需要在应用启动时立即应用存储的主题设置。

解决办法:给 App.vue 加上挂载时就进行一次设置。

onMounted(() => {
  // 根据存储的设置初始化主题
  document.documentElement.setAttribute('data-theme', settingsStore.isDarkMode ? 'dark' : 'light')
})

传输数据

axios、 XMLHttpRequest 、fetch

以下是 AxiosXMLHttpRequestFetch 在发送 HTTP 请求时的对比,包括用法、性能、兼容性和适用场景的详细分析:


前后端通信所用技术对比

AI 对话需要到流式响应处理,通信技术最终采用 fetch。

场景 Axios XMLHttpRequest Fetch
快速开发简单请求 优秀,语法简洁 不适合,代码繁琐 良好,语法简洁
全局配置需求 支持,提供 defaults 配置 不支持 需要手动实现
请求/响应拦截处理 原生支持拦截器 不支持 需要手动实现
上传/下载进度监听 不支持 支持 不支持
兼容旧版浏览器 支持,通过 polyfill 支持 不支持
流式响应处理 不支持 不支持 支持(结合 ReadableStream
轻量级需求 适合 不适合 适合

实现流式数据处理

1. 项目实现代码

  1. 发送请求
    在 src/utils/api.js 中,chatApi.sendMessage 方法负责发送请求。根据 stream 参数的值,决定是否请求流式响应。

    async sendMessage(messages, stream = false) {
    // ...
    const response = await fetch(${API_BASE_URL}/chat/completions, {
    method: 'POST',
    headers: {
    ...createHeaders(),
    ...(stream && { 'Accept': 'text/event-stream' }) // 如果是流式响应,添加相应的 Accept 头
    },
    body: JSON.stringify(payload)
    })
    // ...
    if (stream) {
    return response // 对于流式响应,直接返回 response 对象
    }
    // ...
    }

  2. 处理流式响应
    在 src/utils/messageHandler.js 中,processStreamResponse 方法负责处理流式响应。它使用 ReadableStream 的 getReader 方法逐步读取数据,并使用 TextDecoder 解码数据。

    1. 读取流数据
    2. 解码数据块
    3. 处理解码后的数据:先拆分为行(数组),再转换为json字符串,再转换为js对象,提取出对象中content内容,更新message、更新token使用量

    async processStreamResponse(response, { updateMessage, updateTokenCount }) {
    try {
    let fullResponse = '';
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    // 1.读取流数据
    while (true) {
    const { done, value } = await reader.read();
    if (done) {
    console.log('流式响应完成');
    break;
    }
    //2.解码数据块
    const chunk = decoder.decode(value); //这里每一个chunk是一个可能包含多个数组
    //3.处理解码后的数据,先拆分为行(数组),再转换为json字符串,再转换为js对象,提取出对象中content内容,更新message、更新token使用量

           // 3.1 拆分为行
           const lines = chunk.split('\n').filter(line => line.trim() !== '');
           for (const line of lines) {
               if (line.includes('data: ')) {
           // 3.2 转换为json字符串
                   const jsonStr = line.replace('data: ', '');
                   // 检查是否结束
                   if (jsonStr === '[DONE]') {
                       console.log('流式响应完成,读取完毕');
                       continue;
                   }
           // 3.3 转换为js对象
                   try {
                       const jsData = JSON.parse(jsonStr);
                       if (jsData.choices[0].delta.content) {
                           const content = jsData.choices[0].delta.content;
           //3.4 提取出对象中content内容,更新message
                           fullResponse += content;
    
                           updateMessage(fullResponse);
                       }
    
           // 3.5更新token使用量
                       if (jsData.usage) {
                           updateTokenCount(jsData.usage);
                       }
                   } catch (e) {
                       console.error('解析JSON失败:', e);
                   }
               }
           }
       }
    

    } catch (error) {
    console.error('流处理错误:', error);
    throw error;
    }
    },

  1. 更新界面
    在 src/views/ChatView.vue 中,handleSend 方法调用 processStreamResponse,并通过回调函数 updateMessage 和 updateTokenCount 更新界面。

    const handleSend = async (content) => {
    // ...
    try {
    const response = await chatApi.sendMessage(
    messages.value.slice(0, -1).map(m => ({
    role: m.role,
    content: m.content
    })),
    settingsStore.streamResponse
    );

         if (settingsStore.streamResponse) {
           // 这里使用了await不会将这个变化为同步吗,我了解到的使用await后会等之后的函数调用完再执行之后的代码,是这样吗?
             await messageHandler.processStreamResponse(response, {
                 updateMessage: (content) => chatStore.updateLastMessage(content),
                 updateTokenCount: (usage) => chatStore.updateTokenCount(usage)
             });
         }
         // ...
     } catch (error) {
         chatStore.updateLastMessage('抱歉,发生了错误,请稍后重试。')
     } finally {
         chatStore.isLoading = false
     }
    

    }

2. 知识点

2.1. 疑问及解答

  1. 几个核心概念:1.ReadableStream,2.getReader()算是ReadableStream的一个方法吗,3.reader.read(),4.Uint8Array ,5. Streams API,6.TextDecoder
  2. (由 fetch 返回的response.body 是ReadableStream对象引申出来的问题) fetch 处理响应有哪些方式?类型又是什么 ?
2.1.1. 解答的理解

有关流式涉及到的概念

  1. ReadableStreamStreamsAPI 的核心对象 之一,这里涉及到是因为网络请求的response.body是一个ReadableStream对象。
    1. 深入补充:Streams API是 Web API ,用于流方式处理数据
  1. getReader()ReadableStream的一个方法,(因为 1 所以response.body也有这个方法)

  2. 这个方法会返回一个 reader 对象(这里是简称),它可以逐块读取流中的数据,提供对流的完全控制 (注:使用 getReader 后,其他地方便无法访问流,意思是只有它返回的 reader 对象可以访问流)

  3. reader 对象有一个方法reader.read() 异步读取流中的数据块 。返回值如下:

    { done: true/false, value: Uint8Array | undefined }

    • done: 如果为 true,表示流已读取完毕。
    • value: 当前读取的块数据,通常是 Uint8Array, 每个元素占用 1 个字节 。
  1. Uint8Array 是一种 类型化数组,高效地处理原始二进制数据,如文件块、图像、网络响应 。
  2. 对二进制数据的处理:
    1. Uint8Array转换为字符串,使用TextDecoder,编码如 utf-8utf-16。使用它的decode方法将字节数据转换为字符串。
    2. 转换为图像/视频,使用Blob,设置类型 image 或者 video,再将生成的 blob 对象转换为 URL,即可使用。

总结图示:

Streams API  
├── ReadableStream(response.body类型) → 提供流式数据块
│    ├── getReader() → 获取 reader
│    │    └── reader.read() → 读取单个数据块 {done, value}
│    │
│    └── 数据块通常是 Uint8Array 类型
│
└── TextDecoder → 解码 Uint8Array 为字符串
└── Blob → 解码 Uint8Array 为图像视频

fetch 响应方法极其类型

方法 返回类型 常见场景
response.text() Promise<string> 文本、HTML
response.json() Promise<Object> JSON API 响应
response.blob() Promise<Blob> 图片、视频、文件下载
response.arrayBuffer() Promise<ArrayBuffer> 二进制数据、文件解析
response.formData() Promise<FormData> 表单响应(少见)
response.body ReadableStream 实时处理、进度跟踪

注意:

  • fetch 的响应体只能被读取一次 (即:不能同时调用 response.json()response.text()等 )

  • 即使响应有错误状态(如 404),fetch 不会抛出异常,需要手动检查 response.ok。如下代码处理方式:

    let response = await fetch('https://example.com');
    if (!response.ok) {
    throw new Error(HTTP error! status: ${response.status});
    }

完整代码见:github

相关推荐
程序猿人大林15 分钟前
WPF 关于界面UI菜单权限(或者任意控件的显示权限)的简单管理--只是简单简单简单简单
javascript·ui·wpf
横冲直撞de17 分钟前
客户端(浏览器)vue3本地预览txt,doc,docx,pptx,pdf,xlsx,csv,
开发语言·javascript·pdf
Domain-zhuo26 分钟前
Webpack是什么?
前端·javascript·webpack·前端框架·node.js·ecmascript
学前端的小朱29 分钟前
webpack处理图片资源
前端·webpack·node.js
GISer_Jing30 分钟前
前端工程化(三)
前端
空中楼阁,梦幻泡影40 分钟前
Vue的渲染机制深度解析
前端·javascript·vue.js
木子七1 小时前
NodeJs-包管理工具
前端·nodejs
树上有只程序猿1 小时前
年底了公司要裁员,大家还好吗?
前端·后端
问道飞鱼2 小时前
【前端知识】Javascript进阶-类和继承
开发语言·前端·javascript·继承·
Grocery store owner2 小时前
el-time-picker选择时分秒并且根据总秒数禁用不可选
前端·javascript·vue.js