Claude前端美化实战:从零构建优雅用户界面的完整指南

1次阅读
没有评论

共计 3749 个字符,预计需要花费 10 分钟才能阅读完成。

image.webp

原生 API 的展示痛点

使用 Claude API 时,前端开发者常遇到以下典型问题:

Claude 前端美化实战:从零构建优雅用户界面的完整指南

  • 原始数据格式混乱 :API 返回的 Markdown 文本直接渲染时会出现未解析的符号(如**# 等)
  • 缺乏视觉层次:连续对话堆砌成纯文字段落,用户难以区分对话边界
  • 零交互反馈:没有消息发送状态、加载动画或错误提示
  • 移动端适配缺失:固定宽度布局导致小屏设备出现横向滚动条

技术选型:为什么是 Vue + Element UI

对比主流框架在聊天场景的表现:

  1. React
  2. 优势:生态丰富,性能优化手段成熟
  3. 劣势:JSX 对动态 Markdown 处理较复杂,状态管理学习曲线陡峭

  4. Angular

  5. 优势:强类型支持完善,适合大型应用
  6. 劣势:打包体积大,响应式变更检测在频繁更新的聊天场景开销较高

  7. Vue 3

  8. 单文件组件天然隔离消息 UI 逻辑
  9. Composition API 方便封装消息处理 hook
  10. 配合 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 返回内容的语义(如代码、数字、日期等)实现动态高亮?可以考虑:

  1. 使用 NLP 识别实体类型
  2. 为不同语义单元设计 CSS 样式方案
  3. 实现动态样式注入机制

期待读者在评论区分享自己的实现方案。

正文完
 0
评论(没有评论)