普通视图

发现新文章,点击刷新页面。
昨天 — 2025年7月1日首页

从SSE到打字机——AI场景下前端的实现逻辑与实践

2025年7月1日 19:34

随着Deepseek的横空出世,让每个人都有了构建自己AI知识库的机会,作为一个前端开发者,完全可以通过大模型构建自己的日常学习知识库,然后自己写一个AI的交互页面构建自己的 ChatGPT ,当然说到这,肯定有人说现在有一键构建的开源项目为什么不用呢,说白了技术还是要自己实现才能更加深入地理解,并且更加灵活地运用到日常学习或者实际业务场景中去。

本篇文章只从前端的角度出发,分析实现一个AI的交互页面能用到哪些技术,最后再去实现一个AI场景页面。

当然,你也可以点击这里直接查看本篇文章实现的页面。

如果打不开,这里还有贴心国内服务器的备用链接

PS:上面两个演示链接都是用 vuepress 实现的个人博客,感觉用这套框架实现自定义组件里面的坑还挺多了,有机会可以再写一篇关于 vuepress 的开发避坑文章。

当然,关于IM的交互逻辑在我之前的文章 【从零开始实现一个腾讯IM即时通讯组件(无UI设计方案)~】中已经详细描述了实现过程,所以,这篇文章就从已经实现了IM交互的页面基础上开始实现AI场景下的IM。

技术选型

涉及到AI场景必然会联想到打字机效果的流式输出文本,那么前端实现这种效果有哪些方式呢?

协议对比

首先最简单的,通过轮询接口不断获取数据,其次通过websocket不断获取监听到的数据,最后通过服务端消息推送获取数据。这三种思路对应着三种通讯协议:HTTP、WebSocket、SSE。

先对比一下这三种协议:

基本概念与通信模式

特性 HTTP SSE (Server-Sent Events) WebSocket
协议类型 无状态的请求 - 响应协议 基于 HTTP 的单向事件流协议 基于 TCP 的全双工实时协议
通信方向 客户端→服务器(单向) 服务器→客户端(单向) 双向(全双工)
连接特性 短连接(每次请求新建连接) 长连接(单次请求,持续响应) 长连接(一次握手,持续通信)
发起方式 客户端主动请求 客户端主动请求,服务器持续推送 客户端发起握手,后续双向通信
典型场景 静态资源请求、API 调用 实时通知、股票行情、新闻推送 实时聊天、在线游戏、协作工具

技术细节对比

特性 HTTP SSE WebSocket
协议基础 HTTP/1.1 或 HTTP/2 HTTP/1.1 或 HTTP/2 WebSocket 协议 (RFC 6455)
端口 80 (HTTP) / 443 (HTTPS) 80/443 80 (ws) / 443 (wss)
数据格式 文本、JSON、二进制等 纯文本(text/event-stream) 文本或二进制(帧格式)
二进制支持 支持,但需额外处理 不支持(需编码为文本) 原生支持
自动重连 否(需客户端实现) 是(内置机制) 否(需手动实现)
心跳机制 否(需轮询) 否(需自定义) 是(Ping/Pong 帧)
浏览器兼容性 全兼容 现代浏览器(IE 不支持) 现代浏览器(IE 10+)

性能与效率

特性 HTTP SSE WebSocket
连接开销 高(每次请求需重新建立连接) 中(一次连接,长期保持) 低(一次握手,持续通信)
协议 overhead 高(HTTP 头信息冗余) 低(仅初始头) 中(帧头开销较小)
实时性 低(依赖客户端轮询) 高(服务器主动推送) 极高(双向实时)
带宽利用率 低(轮询导致无效请求) 中(单向持续传输) 高(按需双向传输)
延迟 高(请求响应周期) 中(推送延迟) 低(长连接直接通信)

API选择

再来回看一下我们的需求,AI场景说白了一问一答的方式,那么我们希望发送一次请求后,能够持续获取数据,本次请求后端也只需要知道我的问题即可,不需要和前端进行其他交互,所以 SSE 在这种场景下的优势就显而易见了。

前端要在浏览器中实现 SSE 的方式有两种:

  • EventSource API
  • fetch API

EventSourcefetch 都是现代 Web 开发中用于与服务器通信的 API。

特性 EventSource (SSE) Fetch API
通信模式 单向(服务器→客户端) 双向(请求→响应)
连接特性 长连接(持续接收服务器推送) 短连接(每次请求新建连接)
数据流类型 事件流(持续不断) 一次性响应(请求完成即结束)
数据格式 文本(事件流格式) 任意(JSON、Blob、文本等)
自动重连 内置支持(自动重连机制) 需手动实现

EventSource API实现了 SSE 。换句话说 EventSource API是 Web 内容与服务器发送事件通信的接口。一个EventSource 实例会对HTTP服务器开启一个持久化的连接,以 text/event-stream 格式发送事件,此连接会一直保持开启直到通过调用 EventSource.close() 关闭。

但是它有一些限制:

  • 无法传递请求体 request body ,必须将执行请求所需的所有信息编码到 URL 中,而大多数浏览器对 URL 的长度限制为 2000 个字符。
  • 无法传递自定义请求头。
  • 只能进行 GET 请求,无法指定其他方法。
  • 如果连接中断,无法控制重试策略,浏览器会自动进行几次尝试然后停止。

而AI场景常常会有一些其他需求,如上文记忆、接口 token 验证等等,于是 fetch 成了我们的最佳选择。

fetch API可以通过设置 headers 支持流式数据的接收,然后通过 ReadableStreamDefaultReader 对象,逐块读取响应的数据。

大模型选择

作为前端开发我们更注重于模型的定制化配置和页面的展示效果与交互,通过第三方模型可以快速满足我们的需求,这里我选用的是阿里云百炼

它直接提供了支持流式输出的接口,只需要在请求头加上 X-DashScope-SSE:true 。比较坑的是阿里云文档里面只提供了 node 的写法,实际浏览器中 axios 并不支持流式传输。

image-20250621145616487

API解析

AbortController

前面我们说到 SSE 的数据传输是单向的,有时候我们会想中断推送信息的接收,实际需求就是中断AI当前回答,所以我们需要一个控制器来更加精细地控制我们的请求。

AbortController 对象的作用是对一个或多个 Web 请求进行中止操作,像 fetch 请求、ReadableStream 以及第三方库的操作都可以取消。

核心机制:借助 AbortSignal 来实现操作的中止。AbortController 会生成一个信号对象,该对象可被传递给请求,当调用 abort() 方法时,就会触发请求的取消操作。

有了这个API我们就可以实现中断回答按钮的实现。

const controller = new AbortController()
const response = await fetch(
  url: 'url',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer token`,
      'X-DashScope-SSE': 'enable', // 允许实时推送
    },
    signal: controller.signal, // 信号对象绑定请求
    body: "{...}",
  }
)

setTimeout(()=> controller.abort(), 1000) // 一秒钟后自动中断请求

Reader

在请求发出后,我们需要一个能持续获取推送信息的入口,fetchresponse.body.getReaderJavaScript 中用于处理 fetch 请求响应流的方法,它允许你以可控的方式逐块读取响应数据,而非一次性处理整个响应。这在处理大文件下载、实时数据流(如视频、SSE)或需要渐进式解析数据的场景中特别有用。

// 获取一个 ReadableStreamDefaultReader 对象,用于逐块读取响应的二进制数据(Uint8Array)。
const reader = response.body.getReader()

while (true) {
  // 读取数据块 流是一次性的,读取完成后无法再次读取
const {done, value} = await reader.read();
  if (done) {
    console.log('流读取完成');
    break;
  }
}

循环调用 read() 以达到获取完整数据的需求,根据 done 判断是否已经读取完毕。

TextDecoder

TextDecoderJavaScript 中用于将二进制数据(如 ArrayBufferUint8Array)解码为人类可读的文字字符串的内置对象。它支持多种字符编码(如 UTF-8ISO-8859-1GBK 等),是处理网络响应、文件读取等二进制数据转换的标准工具。

// 任意二进制数据
const value = ...

// 流式解码:支持分块处理二进制数据(通过多次调用 decode 方法)。
const decoder = new TextDecoder('UTF-8')
// 解码二进制数据为文本
const chunk = decoder.decode(value, { stream: true })

值得注意的是 decodestream 参数设置为 true ,这是为了防止乱码的情况,因为我们知道 UTF-8 是一种变长编码,ASCII 字符(0-127)用 1 个字节表示,而其他字符(如中文、 emoji)可能用 2-4 个字节表示。例如:

  • 的 UTF-8 编码是 [228, 184, 150](3 个字节)。
  • 😊 的 UTF-8 编码是 [240, 159, 152, 138](4 个字节)。

当数据分块传输时,一个字符可能被截断在不同的块中。例如:

块1: [228, 184]    // "中" 的前两个字节(不完整)
块2: [150]         // "中" 的最后一个字节

stream 选项决定了解码器如何处理可能不完整的多字节字符:

stream 行为描述
false 默认值。假设输入是完整的,直接解码所有字节。若遇到不完整字符,会用 替换。
true 假设输入是数据流的一部分,保留未完成的多字节字符,等待后续数据。

实际情况可以参考下段代码:

// 错误情况
const decoder = new TextDecoder();
const chunk1 = new Uint8Array([228, 184]); // "中" 的前两个字节
const chunk2 = new Uint8Array([150]);      // "中" 的最后一个字节

console.log(decoder.decode(chunk1)); // 输出: "�"(错误:截断的字符被替换为乱码)
console.log(decoder.decode(chunk2)); // 输出: "�"(错误:单独的第三个字节无法组成有效字符)

// 正确情况

const decoder = new TextDecoder();
const chunk1 = new Uint8Array([228, 184]); // "中" 的前两个字节
const chunk2 = new Uint8Array([150]);      // "中" 的最后一个字节

console.log(decoder.decode(chunk1, { stream: true })); // 输出: ""(无输出,保留未完成字符)
console.log(decoder.decode(chunk2));                   // 输出: "中"(合并后正确解码)

处理流式输出

结合上述API的分析,fetch 实现处理流式数据的代码如下:

const controller = new AbortController()

const response = await fetch(
  url,
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer sk-dd0e8892eb0445149fd21fd9b1d6176c`,
      'X-DashScope-SSE': 'enable',
    },
    signal: controller.signal,
    body: JSON.stringify({
      input: {
        prompt: text
      },
      parameters: {
        'incremental_output' : 'true' // 增量输出
      },
    }),
  }
)
if (!response.ok) {
  message.error('AI返回错误')
  loadingSend.value = false
  return
}

const decoder = new TextDecoder('UTF-8')
const reader = response.body.getReader()

while (true) {
  const {done, value} = await reader.read();

  if (done) {
    console.log('流读取完成');
    // 中断fetch请求
    controller.abort()
    // 资源释放:释放读取器锁
    reader.releaseLock()
    break;
  }

  // 解码二进制数据为文本
  const chunk = decoder.decode(value, { stream: true })
  console.log('chunk:===>', chunk)
}

处理流式数据

通过 reader 读取到的数据经过 decoder 处理后格式如下:

id:1
event:result
:HTTP_STATUS/200
data:{"output":{"session_id":"0837b503363c4525a6609f868f3f6afa","finish_reason":"null","text":"我是","reject_status":false},"usage":{"models":[{"input_tokens":370,"output_tokens":1,"model_id":"deepseek-v3"}]},"request_id":"ecea2ce7-3867-9074-aa67-92b39ba9253a"}

id:2
event:result
:HTTP_STATUS/200
data:{"output":{"session_id":"0837b503363c4525a6609f868f3f6afa","finish_reason":"null","text":"你的","reject_status":false},"usage":{"models":[{"input_tokens":370,"output_tokens":2,"model_id":"deepseek-v3"}]},"request_id":"ecea2ce7-3867-9074-aa67-92b39ba9253a"}

当然这个是阿里云的返回格式,但流式数据格式都大差不差,接下来我们来分析这段文本。

首先,reader 获取的数据可能会有多段,如上文中的就是 id:1id:2 两段数据。

其中关键字段为:data.output.text ,所以我们需要根据返回数据的结构特点通过正则把有效信息给过滤出来。

// 全局贪婪匹配 "text":" 到 ","reject_status": 之间的内容,确保多段数据也能准确提取所有的有效信息
const regex = /"text":"(.*?)","reject_status":/gs;

这里使用正则而不是 JSON 化的原因是流式数据的处理讲究高效与准确JSON 化更加地消耗性能,而且存在异常报错的可能,为了最大可能保证主流程的持续输出,用正则是更优的选择。当然具体业务场景具体处理,这里仅作个人见解。

根据上述正则,实现一个数据处理函数:

const extractText = (jsonString) => {
  try {
    const regex = /"text":"(.*?)","reject_status":/gs;
    let match;
    let result = '';
    // 利用regex.exec()在字符串里循环查找所有匹配结果,把每次匹配得到的捕获组内容(也就是text字段的值)添加到result字符串中。
    while ((match = regex.exec(jsonString)) !== null) {
      // 将字符串里的\n转义序列转换为真正的换行符,把\"转义序列还原为普通的双引号。
      result += match[1].replace(/\\n/g, '\n').replace(/\\"/g, '"');
    }
    return result
  } catch (error) {
    console.log('error', error)
    return ''
  }
}

最后把数据处理函数加到流式输出代码中,通过缓存持续获取有用的信息:

...
// 用于累计接收到的数据
let accumulatedText = ''

while (true) {
  const {done, value} = await reader.read();

  if (done) {
    ...
    break;
  }

  const chunk = decoder.decode(value, { stream: true })
  // 累加并渲染数据
  const newText = extractText(chunk)
  if (newText) {
  accumulatedText += newText
  }
}

转换MD文本

这里用到几个库来实现:

  • markdown-it 一个快速、功能丰富的 Markdown 解析器,基于 JavaScript 实现。它的主要作用是把 Markdown 文本转换成 HTML。
  • @vscode/markdown-it-katex VS Code 团队开发的插件,用于在 Markdown 中渲染 LaTeX 数学公式,它集成了 KaTeX 这个快速的数学公式渲染引擎。
  • markdown-it-link-attributes 为 Markdown 中的链接添加自定义属性,比如为外部链接添加target="_blank"rel="noopener noreferrer"属性。
  • mermaid-it-markdown 用于在 Markdown 中集成 Mermaid 图表,Mermaid 是一种用文本语法描述图表的工具。

三方库使用

结合上述各种库结合,处理接口返回的信息流:

import MarkdownIt from 'markdown-it'
import MdKatex from '@vscode/markdown-it-katex'
import MdLinkAttributes from 'markdown-it-link-attributes'
import MdMermaid from 'mermaid-it-markdown'
import hljs from 'highlight.js'

const mdi = new MarkdownIt({
  html: false,
  linkify: true,
  highlight(code, language) {
    const validLang = !!(language && hljs.getLanguage(language))
    if (validLang) {
      const lang = language ?? ''
      return highlightBlock(hljs.highlight(code, { language: lang }).value, lang)
    }
    return highlightBlock(hljs.highlightAuto(code).value, '')
  },
})
mdi.use(MdLinkAttributes, { attrs: { target: '_blank', rel: 'noopener' } }).use(MdKatex).use(MdMermaid)

// 实现代码块快速复制
function highlightBlock(str, lang) {
  return `<pre class="code-block-wrapper">
            <div class="code-block-header">
                <span class="code-block-header__lang">${lang}</span>
                <span class="code-block-header__copy">复制代码</span>
            </div>
            <code class="hljs code-block-body ${lang}"><br>${str}</code>
          </pre>`
}

const renderToAI = (text) => {
  // 对数学公式进行处理,自动添加 $$ 符号
  const escapedText = escapeBrackets(escapeDollarNumber(text))
  return mdi.render(escapedText)
}

const escapeBrackets = (text) => {
  const pattern = /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\]|\\\((.*?)\\\)/g
  return text.replace(pattern, (match, codeBlock, squareBracket, roundBracket) => {
    if (codeBlock)
      return codeBlock
    else if (squareBracket)
      return `$$${squareBracket}$$`
    else if (roundBracket)
      return `$${roundBracket}$`
    return match
  })
}

const escapeDollarNumber = (text) => {
  let escapedText = ''

  for (let i = 0; i < text.length; i += 1) {
    let char = text[i]
    const nextChar = text[i + 1] || ' '

    if (char === '$' && nextChar >= '0' && nextChar <= '9')
      char = '\\$'

    escapedText += char
  }

  return escapedText
}

复制代码块

快速复制代码实现:

// 聊天列表主体元素
const textRef = ref()

// 构建textarea,将内容复制到剪切板
const copyToClip = (text) => {
  return new Promise((resolve, reject) => {
    try {
      const input = document.createElement('textarea')
      input.setAttribute('readonly', 'readonly')
      input.value = text
      document.body.appendChild(input)
      input.select()
      if (document.execCommand('copy'))
        document.execCommand('copy')
      document.body.removeChild(input)
      resolve(text)
    }
    catch (error) {
      reject(error)
    }
  })
}

// 为所有的复制代码按钮添加复制事件
const addCopyEvents = () => {
  if (textRef.value) {
    const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy')
    copyBtn.forEach((btn) => {
      btn.addEventListener('click', () => {
        const code = btn.parentElement?.nextElementSibling?.textContent
        if (code) {
          copyToClip(code).then(() => {
            btn.textContent = '复制成功'
            setTimeout(() => {
              btn.textContent = '复制代码'
            }, 1000)
          })
        }
      })
    })
  }
}

// 移除页面中所有的复制事件
const removeCopyEvents = () => {
  if (textRef.value) {
    const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy')
    copyBtn.forEach((btn) => {
      btn.removeEventListener('click', () => { })
    })
  }
}

// 在合适的生命周期里注册或卸载重新事件

// 可以在流式输出完成,页面渲染完成的时候手动调用,避免性能浪费,更加合理
onUpdated(() => {
  addCopyEvents()
})

onUnmounted(() => {
  removeCopyEvents()
})

自定义MD样式

MD样式:

.ai-message {
    background-color: transparent;
    font-size: 14px;
}
.ai-message p {
    white-space: pre-wrap;
}
.ai-message ol {
    list-style-type: decimal;
}
.ai-message ul {
    list-style-type: disc;
}
.ai-message pre code,
.ai-message pre tt {
    line-height: 1.65;
}
.ai-message .highlight pre,
.ai-message pre {
    background-color: #fff;
}
.ai-message code.hljs {
    padding: 0;
}
.ai-message .code-block-wrapper {
    position: relative;
    padding: 0 12px;
    border-radius: 8px;
}
.ai-message .code-block-header {
    position: absolute;
    top: 5px;
    right: 0;
    width: 100%;
    padding: 0 1rem;
    display: flex;
    justify-content: flex-end;
    align-items: center;
    color: #b3b3b3;
}
.ai-message .code-block-header__copy {
    cursor: pointer;
    margin-left: 0.5rem;
    user-select: none;
}
.ai-message .code-block-header__copy:hover {
    color: #65a665;
}
.ai-message div[id^='mermaid-container'] {
    padding: 4px;
    border-radius: 4px;
    overflow-x: auto !important;
    background-color: #fff;
    border: 1px solid #e5e5e5;
}
.ai-message li {
    margin-left: 16px;
    box-sizing: border-box;
}

最后,把处理函数追加到处理流式数据后面:

let mdHtml = ''

...
const chunk = decoder.decode(value, { stream: true })
const newText = extractText(chunk)
if (newText) {
  accumulatedText += newText
  mdHtml += renderToAI(accumulatedText)
}

打字机

到目前为止我们已经流式地拿到了接口返回的数据并且转换成了页面可以展示的MD风格HTML字符串。

打字机的基本思路就是按照一定频率把内容添加到页面上,并且在内容最前面加个打字的光标。

直接上代码:

<template>
  <div v-html="displayText + `${ showCursor || adding ? `<span class='cursors'>_</span>`:'' }`"></div>
</template>

<script setup>
import { ref, watch, onUnmounted } from 'vue';

const props = defineProps({
  // 要显示的完整文本
  text: {
    type: String,
    required: true
  },
  // 打字速度(毫秒/字符)
  speed: {
    type: Number,
    default: 10
  },
  showCursor: {
    type: Boolean,
    default: false
  },
  break: {
    type: Boolean,
    default: false
  },
});
const emits = defineEmits(['update', 'ok'])

const displayText = ref('');
const adding = ref(false);
let timer = null;

// 更新显示的文本
const updateDisplayText = () => {
  if (displayText.value.length < props.text.length) {
    adding.value = true;
    displayText.value = props.text.substring(0, displayText.value.length + 1);
    emits('update')
    timer = setTimeout(updateDisplayText, props.speed);
  } else {
    adding.value = false;
    setTimeout(() =>{
      emits('ok')
    } ,600)
  }
};

// 增量更新
watch(() => props.text, (newText) => {
  // 如果新文本比当前显示的文本长,则继续打字
  if (newText.length > displayText.value.length) {
    clearTimeout(timer);
    updateDisplayText();
  }
});

// 停止回答
watch(() => props.break, (val) => {
  if (val) {
    displayText.value = props.text + ''
    clearTimeout(timer);
    adding.value = false;
    setTimeout(() =>{
      emits('ok')
    } ,600)
  }
});

// 初始化
updateDisplayText();

// 组件卸载时清理定时器
onUnmounted(() => {
  clearTimeout(timer);
});
</script>

<style>

.cursors {
  font-weight: 700;
  vertical-align: baseline;
  animation: blink 1s infinite;
  color: #3a5ccc;
}

@keyframes blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}
</style>  

我们只需要把上述转换的MD文本传入这个组件就能实现打字机效果。

<temlate>
<div class="ai-message">
    <TypingEffect :text="text" :showCursor="!ready" :break="break" @update="updateAIText" @ok="textAllShow" />
  </div>
</temlate>

需要注意的是,打字机打印的速度是按照恒定速度执行的,流式数据是不规则时间返回的,有可能返回很快,也有可能返回很慢,所以两边就会有时间差。

这就造成了一种现象,有时候我们点了停止回答的按钮,页面上还在不断输出内容,好像没有打断这次回答,这里我们只需要在点击停止回答的时候终止打字机的轮询,直接展示完整数据即可。

最后优化显示,需要自动滚动到底部:

const scrollToBottom = () => {
  try {
    const { height } = textRef.value.getBoundingClientRect()
    textRef.value.scrollTo({
      top: textRef.value.scrollHeight - height,
      behavior: 'smooth',
    })
  } catch (e) {}
}

总结

前端AI场景下总结来说就两个平时不常见的技术点:

  • 流式输出
  • 请求中断

当然本篇文章只是实现了基本的AI场景,像上下文记忆、多对话框以及更大模型的微调等等并未涉及到,这些更加深入地功能,可以后面慢慢研究,那么,这次就到这里~

昨天以前首页

打造极致聊天体验:uz-chat——全端AI聊天组件来了!

作者 伊泽瑞尔
2025年6月24日 21:47

🚀 打造极致聊天体验:uz-chat——全端AI聊天组件来了!

🌟 插件介绍

uz-chat是一款基于uni-app开发的全端AI聊天组件,可无缝对接DeepSeek、OpenAI等主流AI服务。它不仅支持基础的消息展示,还内置了打字机效果、Markdown渲染和平滑滚动等高级特性,让你的应用瞬间拥有专业级聊天体验!

插件效果

✨ 核心功能亮点

1️⃣ 全端兼容,一次开发多端运行

  • 完美支持H5、小程序、App等多平台
  • 基于uni-app生态,无缝集成现有项目

2️⃣ 流畅的消息交互体验

  • 🎉 实时滚动:新消息自动平滑滚动到底部
  • ⌨️ 打字机效果:模拟AI思考和输入过程
  • 📋 消息操作:支持复制、编辑消息内容

3️⃣ 强大的内容渲染

  • ✍️ Markdown支持:代码高亮、表格、列表等格式化展示
  • 💻 代码块展示:支持多种编程语言语法高亮
  • 📝 富文本内容:满足复杂消息展示需求

4️⃣ 灵活的自定义能力

  • 支持自定义头像、昵称
  • 可扩展的消息类型插槽
  • 丰富的样式定制选项

🚀 快速上手

安装方式

在DCloud插件市场导入聊天消息组件uni_modules版本,无需额外import即可使用。

基础用法

<template>
  <uz-chat 
    @sendMessage="sendMessage"
    :isSending="isSending"
    :messages="messages"
    v-model:modelValue="inputMessage"
    :offset-height="topHeight + 'rpx'"
  ></uz-chat>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const isSending = ref(false)
const messages = ref([])
const inputMessage = ref('')

// 发送消息处理
const sendMessage = async (msg: string) => {
  // 实现消息发送逻辑
}
</script>

对接AI服务

// 对接DeepSeek等AI服务示例
async function createChatCompletion(messages) {
  const openai = new OpenAI({
    baseURL: 'https://api.deepseek.com',
    apiKey: process.env.DEEPSEEK_API_KEY
  })
  
  return openai.chat.completions.create({
    messages: messages,
    model: 'deepseek-chat',
    stream: true
  })
}

🛠️ 技术特性

  • 高效渲染:采用虚拟列表技术,支持大量消息展示
  • 性能优化:消息滚动节流处理,避免卡顿
  • 类型安全:完整的TypeScript类型定义
  • 轻量化设计:核心功能打包体积小

📈 未来规划

  • 支持上拉加载更多历史消息
  • 支持语音消息
  • 自定义表情包功能
  • 暗黑模式

🤝 如何获取

💡 写在最后

uz-chat致力于为开发者提供开箱即用的高质量聊天组件,无论是构建AI助手、在线客服还是社交聊天应用,它都能满足你的需求。现在就集成uz-chat,为你的应用增添专业的聊天体验吧!

如果觉得这个组件对你有帮助,欢迎在掘金、CSDN等平台分享你的使用体验,也欢迎提交issue和PR参与项目贡献!

❌
❌