共计 3749 个字符,预计需要花费 10 分钟才能阅读完成。
原生 API 的展示痛点
使用 Claude API 时,前端开发者常遇到以下典型问题:

- 原始数据格式混乱 :API 返回的 Markdown 文本直接渲染时会出现未解析的符号(如
**、#等) - 缺乏视觉层次:连续对话堆砌成纯文字段落,用户难以区分对话边界
- 零交互反馈:没有消息发送状态、加载动画或错误提示
- 移动端适配缺失:固定宽度布局导致小屏设备出现横向滚动条
技术选型:为什么是 Vue + Element UI
对比主流框架在聊天场景的表现:
- React:
- 优势:生态丰富,性能优化手段成熟
-
劣势:JSX 对动态 Markdown 处理较复杂,状态管理学习曲线陡峭
-
Angular:
- 优势:强类型支持完善,适合大型应用
-
劣势:打包体积大,响应式变更检测在频繁更新的聊天场景开销较高
-
Vue 3:
- 单文件组件天然隔离消息 UI 逻辑
- Composition API 方便封装消息处理 hook
- 配合 Element UI 可快速搭建专业级界面
核心实现
API 请求封装
interface ClaudeMessage {
role: 'user' | 'assistant'
content: string
timestamp: number
}
/**
* 封装 Claude API 请求
* @param prompt - 用户输入内容
* @param history - 对话历史记录
* @param signal - 用于取消请求的 AbortSignal
*/
async function fetchClaudeResponse(
prompt: string,
history: ClaudeMessage[],
signal?: AbortSignal
): Promise<ClaudeMessage> {
const loading = ElLoading.service({
lock: true,
text: 'Claude 正在思考...',
})
try {const { data} = await axios.post<{content: string}>(
'/api/claude',
{prompt, history},
{signal}
)
return {
role: 'assistant',
content: data.content,
timestamp: Date.now(),}
} catch (err) {ElMessage.error('请求失败:' + err.message)
throw err
} finally {loading.close()
}
}
消息气泡组件
<template>
<div
class="message"
:class="[role, {'animate-pop': isNew}]"
@transitionend="isNew = false"
>
<div class="avatar">{{role === 'user' ? '👤' : '🤖'}}</div>
<div class="content">
<MarkdownRenderer :source="content" />
</div>
</div>
</template>
<style scoped>
.message {
display: flex;
gap: 12px;
margin-bottom: 16px;
opacity: 0;
transform: translateY(10px);
transition: all 0.3s ease;
}
.message.animate-pop {
opacity: 1;
transform: none;
}
/* 响应式调整 */
@media (max-width: 768px) {
.message {flex-direction: column;}
}
</style>
Markdown 渲染增强
安装依赖:
npm install markdown-it markdown-it-highlightjs markdown-it-task-lists
配置插件:
import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js'
export const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
highlight: (str, lang) => {if (lang && hljs.getLanguage(lang)) {return `<pre class="hljs"><code>${hljs.highlight(str, { language: lang}).value}</code></pre>`
}
return ''
}
}).use(require('markdown-it-task-lists'))
性能优化
虚拟滚动实现
<template>
<RecycleScroller
class="messages"
:items="messages"
:item-size="80"
key-field="timestamp"
>
<template #default="{item}">
<ChatMessage
:role="item.role"
:content="item.content"
:is-new="item.timestamp > lastSeenTime"
/>
</template>
</RecycleScroller>
</template>
<script setup>
import {RecycleScroller} from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
</script>
IndexedDB 缓存
class ChatDB {
private db: IDBDatabase | null = null
async init() {return new Promise<void>((resolve, reject) => {const request = indexedDB.open('ChatHistory', 1)
request.onupgradeneeded = (e) => {const db = (e.target as IDBOpenDBRequest).result
if (!db.objectStoreNames.contains('messages')) {db.createObjectStore('messages', { keyPath: 'timestamp'})
}
}
request.onsuccess = (e) => {this.db = (e.target as IDBOpenDBRequest).result
resolve()}
request.onerror = reject
})
}
async saveMessage(msg: ClaudeMessage) {if (!this.db) await this.init()
return new Promise((resolve, reject) => {const tx = this.db!.transaction('messages', 'readwrite')
tx.objectStore('messages').add(msg)
tx.oncomplete = resolve
tx.onerror = reject
})
}
}
避坑指南
SSE 连接管理
let eventSource: EventSource | null = null
function startStream() {eventSource = new EventSource('/api/stream')
eventSource.onmessage = (e) => {const partial = JSON.parse(e.data)
updateMessage(partial)
}
eventSource.onerror = () => {eventSource?.close()
ElMessage.warning('连接中断,正在重试...')
setTimeout(startStream, 3000)
}
}
onUnmounted(() => {eventSource?.close()
})
移动端键盘遮挡
<template>
<div class="input-area">
<textarea
ref="input"
v-model="message"
@focus="adjustPosition"
placeholder="输入消息..."
/>
<button @click="send"> 发送 </button>
</div>
</template>
<script setup>
const adjustPosition = () => {setTimeout(() => {
input.value?.scrollIntoView({
behavior: 'smooth',
block: 'center'
})
}, 300)
}
</script>
项目资源与展望
完整代码已托管至 GitHub 仓库:claude-chat-ui,包含:
- 开箱即用的 Vue 3 项目模板
- 预配置的 Element UI 主题
- 完整的 TypeScript 类型定义
思考题:如何基于 Claude 返回内容的语义(如代码、数字、日期等)实现动态高亮?可以考虑:
- 使用 NLP 识别实体类型
- 为不同语义单元设计 CSS 样式方案
- 实现动态样式注入机制
期待读者在评论区分享自己的实现方案。
正文完
发表至: 前端开发
近一天内
