DeepSeek AI 聊天助手集成指南
先看完整效果:
PixPin_2025-02-19_09-15-59
效果图:
目录
- 项目概述
- 功能特点
- 环境准备
- 项目结构
- 组件详解
- ChatContainer
- ChatInput
- MessageBubble
- TypeWriter
- 核心代码示例
- 使用指南
- 常见问题
项目概述
基于 Vue 3 + TypeScript + Element Plus 开发的 AI 聊天助手,集成了 DeepSeek 的 API 服务。提供打字机模式和流式输出两种对话模式,支持多种 AI 模型切换。
功能特点
基础功能 | 交互体验 | UI/UX | 其他特性 |
---|---|---|---|
🚀 Vue 3 Composition API | ✨ 打字机效果 | 🎨 Element Plus UI | 📱 响应式设计 |
💪 TypeScript 支持 | 🌊 流式实时输出 | 🖼️ 自定义用户头像 | 🔄 模型切换 |
🔌 DeepSeek API 集成 | ⌚ 消息时间显示 | 🎭 思考状态动画 | 🗑️ 对话清空 |
环境准备
1. 安装依赖
# 创建项目
npm create vue@latest# 安装依赖
npm install element-plus @element-plus/icons-vue axios
2. 环境配置
创建 .env.local
文件:
VITE_DEEPSEEK_API_KEY=your_api_key_here
项目结构
src/
├── components/chat/ # 聊天相关组件
│ ├── ChatContainer.vue # 聊天容器组件
│ ├── ChatInput.vue # 输入组件
│ ├── MessageBubble.vue # 消息气泡组件
│ └── TypeWriter.vue # 打字机效果组件
├── services/
│ └── aiService.ts # API 服务封装
├── views/
│ ├── ChatView.vue # 打字机模式页面
│ └── StreamView.vue # 流式输出页面
└── App.vue # 根组件
组件详解
1. ChatContainer.vue
聊天界面的核心容器组件。
功能特性
- 📝 消息列表管理
- 🔄 自动滚动控制
- ⌨️ 打字机效果管理
- 🔀 模型切换
- 🗑️ 清空对话
- 💫 思考状态动画
- 📱 响应式适配
组件 API
// Props
interface Props {title?: string // 聊天标题messages: Message[] // 消息列表loading?: boolean // 加载状态streamMode?: boolean // 流式模式
}// Events
interface Events {send: (message: string) => voidclear: () => voidmodelChange: (model: ModelType) => void
}
完整代码如下:
<template><!-- 聊天容器主组件 --><div class="chat-container"><!-- 聊天头部:标题和清空按钮 --><div class="chat-header"><div class="header-left"><h3>{{ title }}</h3><el-select v-model="currentModel" size="small" class="model-select":disabled="loading"><el-optionv-for="(label, model) in modelOptions":key="model":label="label":value="model"/></el-select></div><el-button type="danger" plainclass="clear-button"@click="showClearConfirm"><template #icon><el-icon><Delete /></el-icon></template>清空全部对话</el-button></div><!-- 消息列表区域:包含所有对话内容 --><div class="chat-messages" ref="messagesContainer"><!-- 循环渲染消息气泡 --><MessageBubblev-for="(message, index) in messages":key="index":content="message.content":is-user="message.role === 'user'":use-typewriter="!initialLoad && !streamMode && message.role === 'assistant'"@complete="handleMessageComplete(index)"/><!-- AI思考中状态显示 --><div v-if="loading && (!messages.length || messages[messages.length - 1].role === 'user')" class="message message-ai thinking-message"><div class="message-content"><div class="avatar-wrapper"><el-avatar :size="40" class="ai-avatar"><el-icon><Service /></el-icon></el-avatar></div><div class="bubble-wrapper"><div class="bubble thinking-bubble"><div class="dots-container"><span class="dot"></span><span class="dot"></span><span class="dot"></span></div></div></div></div></div></div><!-- 输入区域组件 --><ChatInput :disabled="loading || isTyping"@send="$emit('send', $event)"/><!-- 确认弹窗 --><el-dialogv-model="showConfirmDialog"title="确认清空"width="400px":show-close="false"class="clear-dialog"><div class="dialog-content"><el-icon class="warning-icon" color="#E6A23C"><Warning /></el-icon><p>确定要清空所有对话记录吗?</p><p class="warning-text">此操作不可恢复</p></div><template #footer><div class="dialog-footer"><el-button @click="showConfirmDialog = false">取消</el-button><el-button type="danger" @click="handleClear">确认清空</el-button></div></template></el-dialog></div>
</template><script setup lang="ts">
import { ref, onMounted, nextTick, watch } from 'vue'
import { Service, Loading, Delete, Warning } from '@element-plus/icons-vue'
import { ModelType } from '@/services/aiService'// 消息类型定义
interface Message {role: 'user' | 'assistant' | 'system'content: string
}// 组件属性定义
const props = defineProps<{title?: string // 聊天标题messages: Message[] // 消息列表loading?: boolean // 加载状态streamMode?: boolean // 新增流式模式属性
}>()// 定义组件事件
const emit = defineEmits<{send: [message: string]clear: []modelChange: [model: ModelType]
}>()// 组件状态
const messagesContainer = ref<HTMLElement | null>(null)
const isTyping = ref(false)
const initialLoad = ref(true) // 添加初始加载标记// 确认弹窗状态
const showConfirmDialog = ref(false)// 模型选项
const modelOptions = {[ModelType.Chat]: 'DeepSeek-V3 (通用对话)',[ModelType.Reasoner]: 'DeepSeek-R1 (推理增强)'
}// 当前选择的模型
const currentModel = ref<ModelType>(ModelType.Chat)// 处理消息打字完成事件
const handleMessageComplete = (index: number) => {if (index === props.messages.length - 1) {isTyping.value = false}
}// 监听新消息,控制打字机效果
watch(() => props.messages, (newMessages, oldMessages) => {// 跳过初始加载的消息if (initialLoad.value) {initialLoad.value = falsereturn}if (newMessages.length > oldMessages?.length) {const lastMessage = newMessages[newMessages.length - 1]// 只在非流式模式下启用打字机效果if (lastMessage.role === 'assistant' && !props.streamMode) {isTyping.value = true}}
}, { deep: true })// 监听模型变化
watch(currentModel, (newModel) => {emit('modelChange', newModel)
})// 滚动到底部方法
const scrollToBottom = async () => {await nextTick()if (messagesContainer.value) {messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight}
}// 监听消息变化,自动滚动
watch(() => props.messages, scrollToBottom, { deep: true })// 组件挂载时滚动到底部
onMounted(() => {scrollToBottom()// 初始加载完成后重置标记nextTick(() => {initialLoad.value = false})
})// 显示确认弹窗
const showClearConfirm = () => {showConfirmDialog.value = true
}// 处理清空操作
const handleClear = () => {showConfirmDialog.value = falseemit('clear')
}
</script><style scoped>
/* 容器基础样式 */
.chat-container {display: flex;flex-direction: column;height: 100%;width: 100%;max-width: 1200px;margin: 0 auto;background: #fff;border-radius: 16px;box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);overflow: hidden;
}/* 头部样式优化 */
.chat-header {display: flex;justify-content: space-between;align-items: center;box-sizing: border-box;padding: 16px 24px; /* 减小内边距 */background: #fff;border-bottom: 1px solid #eee;box-shadow: 0 1px 6px rgba(0, 0, 0, 0.02); /* 减小阴影 */z-index: 1;
}.chat-header h3 {font-size: 18px; /* 减小标题字号 */font-weight: 600;color: #303133;margin: 0;
}/* 在线状态指示器调整 */
.chat-header h3::before {width: 6px; /* 减小指示点大小 */height: 6px;margin-right: 8px;
}/* 消息列表区域样式 */
.chat-messages {flex: 1;overflow-y: auto;padding: 30px;display: flex;flex-direction: column;gap: 24px;background: #f9fafb;background-image: radial-gradient(circle at 25px 25px, rgba(0, 0, 0, 0.02) 2%, transparent 0%),radial-gradient(circle at 75px 75px, rgba(0, 0, 0, 0.02) 2%, transparent 0%);background-size: 100px 100px;scroll-behavior: smooth;scrollbar-width: thin;scrollbar-color: transparent transparent;
}/* 加载动画 */
.loading {animation: rotating 2s linear infinite;margin-right: 8px;
}@keyframes rotating {from { transform: rotate(0deg); }to { transform: rotate(360deg); }
}/* 自定义滚动条样式 */
.chat-messages::-webkit-scrollbar {width: 4px;
}.chat-messages::-webkit-scrollbar-track {background: transparent;
}.chat-messages::-webkit-scrollbar-thumb {background-color: rgba(0, 0, 0, 0.1);border-radius: 4px;transition: all 0.3s ease;
}/* 只在悬停时显示滚动条 */
.chat-messages:hover::-webkit-scrollbar-thumb {background-color: rgba(0, 0, 0, 0.2);
}/* 响应式布局 */
@media (max-width: 1400px) {.chat-container {max-width: 1000px;}
}@media (max-width: 1200px) {.chat-container {height: 100%;margin: 0;border-radius: 0;}.chat-view {padding: 0;}
}/* 思考中状态样式 */
.thinking-message {opacity: 0.8;animation: fadeInUp 0.3s ease-out;
}/* 思考中的气泡样式 */
.thinking-bubble {min-width: 60px;padding: 12px 16px !important;background: rgba(255, 255, 255, 0.9) !important;backdrop-filter: blur(8px);
}/* 跳动点动画 */
.dots-container {display: flex;align-items: center;gap: 6px;height: 20px;padding: 0 4px;
}.dot {width: 8px;height: 8px;background: #67c23a;border-radius: 50%;display: inline-block;opacity: 0.8;animation: bounce 1.4s infinite ease-in-out both;
}.dot:nth-child(1) {animation-delay: -0.32s;
}.dot:nth-child(2) {animation-delay: -0.16s;
}@keyframes bounce {0%, 80%, 100% { transform: scale(0);} 40% { transform: scale(1);}
}@keyframes fadeInUp {from {opacity: 0;transform: translateY(10px);}to {opacity: 0.8;transform: translateY(0);}
}/* 消息位置调整 */
.message-ai.thinking-message {margin: 0;padding-top: 12px;
}.message-ai.thinking-message .message-content {align-items: center;
}.ai-avatar {background: #67c23a;box-shadow: 0 2px 8px rgba(103, 194, 58, 0.2);
}.avatar-wrapper {width: 40px;height: 40px;flex-shrink: 0;
}.bubble-wrapper {display: flex;flex-direction: column;gap: 4px;
}.message {transition: transform 0.3s ease-out;
}.thinking-message {position: sticky;bottom: 30px;margin-top: 20px;z-index: 1;
}/* 清空按钮样式调整 */
.clear-button {padding: 8px 16px; /* 减小按钮内边距 */font-size: 13px; /* 减小字号 */height: 32px; /* 固定高度 */
}.clear-button :deep(.el-icon) {font-size: 14px; /* 减小图标大小 */margin-right: 4px;vertical-align: -1px;
}/* 头部布局间距调整 */
.header-left {gap: 12px; /* 减小间距 */
}/* 优化动画效果 */
@keyframes pulse {0% {transform: scale(0.95);box-shadow: 0 0 0 0 rgba(103, 194, 58, 0.3);}70% {transform: scale(1);box-shadow: 0 0 0 4px rgba(103, 194, 58, 0);}100% {transform: scale(0.95);box-shadow: 0 0 0 0 rgba(103, 194, 58, 0);}
}/* 弹窗样式 */
.clear-dialog :deep(.el-dialog__header) {padding: 20px 24px;margin: 0;border-bottom: 1px solid #eee;
}.clear-dialog :deep(.el-dialog__title) {font-size: 18px;font-weight: 600;
}.clear-dialog :deep(.el-dialog__body) {padding: 30px 24px;
}.clear-dialog :deep(.el-dialog__footer) {padding: 16px 24px;border-top: 1px solid #eee;
}.dialog-content {display: flex;flex-direction: column;align-items: center;text-align: center;
}.warning-icon {font-size: 48px;margin-bottom: 16px;
}.dialog-content p {margin: 0;font-size: 16px;color: #303133;
}.warning-text {margin-top: 8px !important;font-size: 14px !important;color: #909399 !important;
}.dialog-footer {display: flex;justify-content: flex-end;gap: 12px;
}/* 弹窗动画 */
.clear-dialog :deep(.el-overlay) {backdrop-filter: blur(4px);
}.clear-dialog :deep(.el-dialog) {border-radius: 12px;overflow: hidden;box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}/* 按钮样式优化 */
.dialog-footer :deep(.el-button) {padding: 10px 20px;border-radius: 6px;font-weight: 500;
}.dialog-footer :deep(.el-button--danger) {box-shadow: 0 2px 8px rgba(245, 108, 108, 0.2);
}.dialog-footer :deep(.el-button--danger:hover) {transform: translateY(-1px);box-shadow: 0 4px 12px rgba(245, 108, 108, 0.3);
}/* 模型选择器样式优化 */
.model-select {width: 200px;
}:deep(.el-select .el-input__wrapper) {padding: 0 12px; /* 减小内边距 */height: 32px; /* 减小高度 */
}:deep(.el-select .el-input__inner) {font-size: 13px; /* 减小字号 */
}
</style>
2. ChatInput.vue
输入组件特点:
- 自适应文本框高度
- 字数限制和显示
- Enter 快捷发送
- 优雅的加载状态
- 内联发送按钮
- 防重复提交
template
部分
.chat-input
: 组件容器.input-wrapper
: 输入框和按钮的包装容器el-input
: 文本输入框,支持textarea
自动伸缩el-button
: 发送按钮,带Position
图标
script setup
部分
组件属性 props
disabled
:是否禁用输入框(可选)
组件事件 emit
send(message: string)
:发送消息事件
组件状态 ref
message
:输入的消息内容
关键方法
handleSend()
:发送消息,去除前后空格,防止空消息发送
组件使用
<ChatInput :disabled="isLoading" @send="handleSendMessage" />
完整代码如下:
<template><!-- 聊天输入组件 --><div class="chat-input"><div class="input-wrapper"><el-inputv-model="message"type="textarea":maxlength="2000":autosize="{ minRows: 1, maxRows: 4 }"show-word-limitresize="none"placeholder="输入您的问题..."@keyup.enter.exact="handleSend":disabled="disabled"class="custom-input"/><el-button type="primary" :loading="disabled"@click="handleSend":disabled="!message.trim()"class="send-button"><template #icon><el-icon><Position /></el-icon></template>发送</el-button></div></div>
</template><script setup lang="ts">
import { ref } from 'vue'
import { Position } from '@element-plus/icons-vue'// 组件属性定义
const props = defineProps<{disabled?: boolean // 禁用状态
}>()// 定义事件
const emit = defineEmits<{send: [message: string] // 发送消息事件
}>()// 输入内容
const message = ref('')// 发送消息处理
const handleSend = () => {const trimmedMessage = message.value.trim()if (!trimmedMessage || props.disabled) returnemit('send', trimmedMessage)message.value = ''
}
</script><style scoped>
/* 输入区域容器样式 */
.chat-input {padding: 16px 24px;border-top: 1px solid #eee;background: #fff;border-radius: 0 0 16px 16px;position: relative;
}/* 输入框包装器 */
.input-wrapper {position: relative;display: flex;gap: 12px;align-items: flex-start;
}/* 输入框样式优化 */
.custom-input {flex: 1;transition: all 0.3s ease;
}.custom-input :deep(.el-textarea__inner) {padding: 12px 16px;padding-right: 120px; /* 为字数限制留出空间 */font-size: 14px;border-radius: 12px;border: 1px solid #e4e7ed;background: #f9fafb;box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.02);transition: all 0.3s ease;line-height: 1.6;min-height: 48px;resize: none;
}.custom-input :deep(.el-textarea__inner:hover) {background: #fff;border-color: #c0c4cc;
}.custom-input :deep(.el-textarea__inner:focus) {background: #fff;border-color: #409eff;box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}/* 字数限制样式 */
.custom-input :deep(.el-input__count) {position: absolute;right: 12px;bottom: 8px;background: transparent;font-size: 12px;color: #909399;padding: 0;height: auto;line-height: 1;margin: 0;
}/* 发送按钮样式 */
.send-button {padding: 0 24px;font-size: 14px;border-radius: 10px;height: 48px;transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);font-weight: 500;white-space: nowrap;flex-shrink: 0;
}.send-button:not(:disabled) {background: linear-gradient(135deg, #409eff, #3a8ee6);border: none;
}.send-button:not(:disabled):hover {transform: translateY(-1px);box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}.send-button:not(:disabled):active {transform: translateY(0);
}.send-button :deep(.el-icon) {font-size: 16px;margin-right: 4px;vertical-align: -2px;
}/* 禁用状态样式 */
.custom-input :deep(.el-textarea__inner:disabled) {background: #f5f7fa;border-color: #e4e7ed;cursor: not-allowed;opacity: 0.7;
}/* 响应式调整 */
@media (max-width: 768px) {.chat-input {padding: 12px 16px;}.input-wrapper {gap: 8px;}.send-button {padding: 0 16px;height: 48px;}.custom-input :deep(.el-textarea__inner) {padding-right: 90px;}
}/* 超小屏幕隐藏发送按钮文字 */
@media (max-width: 480px) {.send-button {padding: 0;width: 48px;}.send-button :deep(.el-icon) {margin: 0;}.send-button span:not(.el-icon) {display: none;}
}
</style>
3. MessageBubble.vue
消息气泡组件特点:
- 显示消息气泡
- 区分用户消息和 AI 消息
- 支持头像展示
- 支持打字机效果
- 自动格式化时间
template
部分
-
.message
: 主要消息容器,包含用户或 AI 消息 -
.avatar-wrapper
: 头像区域 -
- 消息内容区域
.bubble-wrapper
.bubble
: 消息文本,支持打字机效果.time
: 消息时间
script setup
部分
组件属性 props
content
:消息内容isUser
:是否为用户消息useTypewriter
(可选):是否启用打字机效果
组件事件 emit
complete
:打字机动画完成事件
关键方法
formatTime()
:格式化消息时间(HH:mm
格式)
组件使用
<MessageBubble :content="'你好!这是 AI 回复的消息。'" :isUser="false" :useTypewriter="true" @complete="handleTypingComplete"
/>
完整代码如下:
<template><!-- 消息气泡组件 --><div :class="['message', isUser ? 'message-user' : 'message-ai']"><div class="message-content"><!-- 头像区域 --><div class="avatar-wrapper"><el-avatar :size="40" :class="isUser ? 'user-avatar' : 'ai-avatar'":src="isUser ? userAvatar : undefined"><el-icon v-if="isUser && !userAvatar"><User /></el-icon><el-icon v-if="!isUser"><Service /></el-icon></el-avatar></div><!-- 消息内容区域 --><div class="bubble-wrapper"><div class="bubble"><TypeWriterv-if="!isUser && useTypewriter":text="content":speed="30"@complete="$emit('complete')"/><span v-else>{{ content }}</span></div><!-- 消息时间 --><div class="time">{{ formatTime() }}</div></div></div></div>
</template><script setup lang="ts">
import { User, Service } from '@element-plus/icons-vue'
import userAvatar from '@/assets/user.jpg'// 组件属性定义
defineProps<{content: string // 消息内容isUser: boolean // 是否为用户消息useTypewriter?: boolean // 是否使用打字机效果
}>()// 定义事件
defineEmits<{complete: [] // 打字完成事件
}>()// 格式化时间方法
const formatTime = () => {const now = new Date()return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`
}
</script><style scoped>
/* 消息容器基础样式 */
.message {display: flex;margin-bottom: 20px;animation: fadeIn 0.3s ease-in-out;
}/* 头像样式 */
.message-content {display: flex;align-items: flex-start;gap: 12px;max-width: 70%;
}/* 用户头像样式 */
.avatar-wrapper {width: 40px;height: 40px;flex-shrink: 0;
}.user-avatar {box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
}.user-avatar :deep(img) {object-fit: cover;width: 100%;height: 100%;
}.ai-avatar {background: #67c23a;box-shadow: 0 2px 8px rgba(103, 194, 58, 0.2);
}/* 气泡容器样式 */
.bubble-wrapper {display: flex;flex-direction: column;gap: 4px;
}.message-user {justify-content: flex-end;
}.message-user .message-content {flex-direction: row-reverse;
}/* 消息气泡样式 */
.bubble {padding: 16px 20px;border-radius: 12px;background: #fff;line-height: 1.6;font-size: 15px;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);position: relative;transition: all 0.3s ease;
}.bubble:hover {box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}.message-user .bubble {background: #409eff;color: white;
}/* 时间显示样式 */
.time {font-size: 12px;color: #909399;margin: 0 8px;opacity: 0.8;
}.message-user .time {text-align: right;
}/* 动画效果 */
@keyframes fadeIn {from {opacity: 0;transform: translateY(10px);}to {opacity: 1;transform: translateY(0);}
}
</style>
4. TypeWriter.vue
组件描述
TypeWriter 组件用于实现文本逐字出现的打字机效果,适用于 AI 回复等场景。支持自定义打字速度、延迟时间,并提供事件监听打字过程。
打字机效果组件特点:
- 逐字显示文本
- 自定义打字速度
- 支持延迟启动
- 文本变化时重新播放
- 提供文本更新和完成事件
组件结构
template
部分
<slot>
: 组件支持插槽,默认显示displayText
script setup
部分
组件属性 props
text
:要显示的文本speed
(可选):打字速度(默认30ms
)delay
(可选):延迟启动时间(默认0ms
)
组件事件 emit
complete
:打字完成事件textUpdate
:每次文本更新时触发
组件状态
displayText
:当前已显示的文本currentIndex
:当前打字位置timer
:定时器引用
关键方法
startTyping()
:递归执行逐字显示文本- 监听
props.text
变化,重新播放打字动画 onUnmounted()
清理定时器
组件使用
<TypeWriter :text="'你好,这是打字机效果演示。'" :speed="50" :delay="500"@complete="handleComplete"@textUpdate="handleTextUpdate"
><template #default="{ text }"><span class="custom-style">{{ text }}</span></template>
</TypeWriter>
完整代码如下:
<template><!-- 打字机效果组件 --><div><slot :text="displayText">{{ displayText }}</slot></div>
</template><script setup lang="ts">
import { ref, watch, onUnmounted } from 'vue'// 组件属性定义
const props = defineProps<{text: string // 要显示的文本speed?: number // 打字速度delay?: number // 开始延迟
}>()// 定义事件
const emit = defineEmits<{complete: [] // 打字完成事件textUpdate: [text: string] // 文本更新事件
}>()// 组件状态
const displayText = ref('')
let currentIndex = 0
let timer: number | null = null// 打字效果实现
const startTyping = () => {if (currentIndex < props.text.length) {displayText.value = props.text.slice(0, currentIndex + 1)emit('textUpdate', displayText.value)currentIndex++timer = window.setTimeout(startTyping, props.speed || 30)} else {emit('complete')}
}// 监听文本变化
watch(() => props.text, () => {if (timer) {clearTimeout(timer)}currentIndex = 0displayText.value = ''timer = window.setTimeout(startTyping, props.delay || 0)
}, { immediate: true })// 组件卸载时清理定时器
onUnmounted(() => {if (timer) {clearTimeout(timer)}
})
</script><style scoped>
/* 打字机容器样式 */
.typewriter {display: inline-block;
}
</style>
API 集成
aiService.ts
封装 DeepSeek API 的 AI 聊天服务,支持普通聊天、推理模式、流式响应等功能。提供灵活的模型切换和参数配置:
class AIChatService {// 普通对话请求async chat(messages: ChatCompletionRequestMessage[]) {// ... API 调用实现}// 流式对话请求async streamChat(messages: ChatCompletionRequestMessage[],onChunk: (chunk: string) => void) {// ... 流式 API 调用实现}// 模型配置更新updateConfig(newConfig: Partial<ChatRequestConfig>) {// ... 配置更新逻辑}
}
功能特点
- 支持普通聊天(同步请求)
- 支持推理模型(Reasoner)
- 支持流式响应
- 支持动态更新 API 配置
- 提供错误处理
枚举 ModelType
枚举项 | 说明 |
---|---|
Chat | 普通聊天模型(deepseek-chat ) |
Reasoner | 推理模型(deepseek-reasoner ) |
类方法
方法 | 说明 |
---|---|
chat(messages: ChatCompletionRequestMessage[]) | 发送聊天请求(同步) |
reason(prompt: string) | 使用 Reasoner 模型推理 |
updateConfig(newConfig: Partial<ChatRequestConfig>) | 更新配置 |
streamChat(messages: ChatCompletionRequestMessage[], onChunk: (chunk: string) => void) | 流式聊天 |
streamReason(prompt: string, onChunk: (chunk: string) => void) | 流式推理 |
使用示例
普通聊天
const response = await aiService.chat([{ role: 'user', content: '你好,AI!' }])
console.log(response) // 输出 AI 回复
流式聊天
await aiService.streamChat([{ role: 'user', content: '请介绍一下 Vue 3' }],(chunk) => {console.log('AI 回复片段:', chunk)}
)
推理模式
const result = await aiService.reason('如何优化前端性能?')
console.log(result)
更新配置
aiService.updateConfig({ temperature: 0.9, max_tokens: 1500 })
使用指南
打字机模式
<ChatContainertitle="DeepSeek 打字机模式":messages="messages":loading="loading"@send="handleSend"
/>
流式输出模式
<ChatContainertitle="DeepSeek 流式输出":messages="messages":loading="loading":stream-mode="true"@send="handleStreamSend"
/>
常见问题
API 相关
- ✅ 检查 API Key 配置
- 🌐 确认网络连接
- 🔍 查看控制台错误
- ⚙️ 验证请求参数
界面显示
- 📱 检查响应式布局
- 📏 确认容器高度
- 📜 验证滚动配置
- 🎨 检查样式冲突
性能优化
- 🔍 合理使用 v-show/v-if
- 🔄 避免深度监听
- 📊 优化滚动事件
- 💾 虚拟滚动处理
核心代码示例
1. 打字机效果 (TypeWriter.vue)
<template><span ref="textContainer"></span>
</template><script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";const props = defineProps<{text: string;speed?: number;
}>();const emit = defineEmits<{complete: [];
}>();const textContainer = ref<HTMLElement | null>(null);
let currentIndex = 0;
let timer: number | null = null;const startTyping = () => {if (currentIndex < props.text.length) {if (textContainer.value) {textContainer.value.textContent = props.text.slice(0, currentIndex + 1);}currentIndex++;timer = window.setTimeout(startTyping, props.speed || 30);} else {emit("complete");}
};onMounted(() => {startTyping();
});onUnmounted(() => {if (timer) clearTimeout(timer);
});
</script>
2. 流式输出 (StreamView.vue)
<script setup lang="ts">
const handleSend = async (message: string) => {messages.value.push({role: "user",content: message,});loading.value = true;try {const assistantMessage = {role: "assistant" as const,content: "",};messages.value.push(assistantMessage);// 流式回调处理const streamCallback = (chunk: string) => {assistantMessage.content += chunk;};await aiService.streamChat(messages.value.slice(0, -1), streamCallback);} catch (error) {ElMessage.error("发送消息失败,请重试");messages.value.pop();} finally {loading.value = false;}
};
</script>
3. API 服务 (aiService.ts)
class AIChatService {// 配置定义private config: ChatRequestConfig = {model: "deepseek-chat",temperature: 0.7,max_tokens: 2000,stream: false,system_message: "你是一个友好的中文助手。",};// 普通对话请求async chat(messages: ChatCompletionRequestMessage[]) {try {const response = await axios.post(`${API_CONFIG.baseURL}/v1/chat/completions`,{model: this.config.model,messages: [{ role: "system", content: this.config.system_message },...messages,],temperature: this.config.temperature,max_tokens: this.config.max_tokens,},{headers: {Authorization: `Bearer ${API_CONFIG.apiKey}`,},});return response.data.choices[0].message.content;} catch (error) {throw new Error("聊天服务出错了");}}// 流式对话请求async streamChat(messages: ChatCompletionRequestMessage[],onChunk: (chunk: string) => void) {try {const response = await fetch(`${API_CONFIG.baseURL}/v1/chat/completions`,{method: "POST",headers: {"Content-Type": "application/json",Authorization: `Bearer ${API_CONFIG.apiKey}`,},body: JSON.stringify({model: this.config.model,messages: [{ role: "system", content: this.config.system_message },...messages,],stream: true,}),});const reader = response.body?.getReader();const decoder = new TextDecoder();while (reader) {const { done, value } = await reader.read();if (done) break;const chunk = decoder.decode(value);const lines = chunk.split("\n").filter((line) => line.trim());for (const line of lines) {if (line.startsWith("data: ")) {const data = JSON.parse(line.slice(6));const content = data.choices[0].delta.content;if (content) onChunk(content);}}}} catch (error) {throw new Error("流式聊天服务出错了");}}
}
4. 思考动画 (ChatContainer.vue)
<template><div v-if="loading" class="thinking-message"><div class="message-content"><el-avatar class="ai-avatar"><el-icon><Service /></el-icon></el-avatar><div class="bubble thinking-bubble"><div class="dots-container"><span class="dot"></span><span class="dot"></span><span class="dot"></span></div></div></div></div>
</template><style scoped>
.thinking-message {opacity: 0.8;animation: fadeInUp 0.3s ease-out;
}.thinking-bubble {min-width: 60px;padding: 12px 16px;background: rgba(255, 255, 255, 0.9);backdrop-filter: blur(8px);
}.dots-container {display: flex;align-items: center;gap: 6px;height: 20px;
}.dot {width: 8px;height: 8px;background: #67c23a;border-radius: 50%;opacity: 0.8;animation: bounce 1.4s infinite ease-in-out both;
}@keyframes bounce {0%,80%,100% {transform: scale(0);}40% {transform: scale(1);}
}@keyframes fadeInUp {from {opacity: 0;transform: translateY(10px);}to {opacity: 0.8;transform: translateY(0);}
}
</style>