首页 / 知识库 / 0基础入门-阅读资料 / 0基础-前端入门

第五章:综合实战 — 用 Vue 搭建 AI 对话应用

这是整个教程的最终 Boss 关。我们要把前四章学到的 HTML、CSS、JavaScript、Vue 全部串起来,做一个类似 ChatGPT 的 AI 对话页面。

做完这个项目,你就真正具备了 AI 应用的前端开发能力。


5.1 项目初始化

打开终端,执行以下命令创建项目:

npm create vue@latest ai-chat

创建时的选项,全部选 No(直接回车)就行:

✔ Add TypeScript? … No
✔ Add JSX Support? … No
✔ Add Vue Router? … No
✔ Add Pinia? … No
✔ Add Vitest? … No
✔ Add ESLint? … No
✔ Add Prettier? … No

然后进入项目,安装依赖并启动:

cd ai-chat
npm install
npm run dev

浏览器打开 http://localhost:5173,看到 Vue 的欢迎页面就说明成功了。

清理项目

把默认的示例文件清理掉,给我们的项目一个干净的起点。

删除这些文件/文件夹:

  • src/components/ 下的所有文件
  • src/assets/ 下的所有文件

src/App.vue 替换成:

<template>
  <div class="app">
    <h1>AI Chat</h1>
  </div>
</template>

<script setup>
</script>

<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: #f5f5f5;
}
</style>

src/main.js 简化成:

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

保存后看浏览器,应该只显示一个”AI Chat”标题,干干净净。


5.2 整体布局设计

我们的页面分三块,像一个三明治:

┌──────────────────────────────┐
│          NavBar              │  ← 顶部导航栏(固定)
├──────────────────────────────┤
│                              │
│         ChatArea             │  ← 中间消息区(可滚动)
│                              │
│                              │
├──────────────────────────────┤
│         InputBar             │  ← 底部输入栏(固定)
└──────────────────────────────┘

这就是一个经典的 Flex 纵向布局,整个页面撑满屏幕,中间消息区自动填满剩余空间。

先更新 App.vue,把骨架搭出来:

<template>
  <div class="app">
    <!-- 顶部导航 -->
    <header class="navbar">
      <span class="logo">🤖 AI Chat</span>
    </header>

    <!-- 中间消息区 -->
    <main class="chat-area">
      <p style="color: #999; text-align: center; margin-top: 40vh;">
        发送一条消息开始对话吧
      </p>
    </main>

    <!-- 底部输入栏 -->
    <footer class="input-bar">
      <input class="input" placeholder="输入你的问题..." />
      <button class="send-btn">发送</button>
    </footer>
  </div>
</template>

<script setup>
</script>

<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.app {
  display: flex;
  flex-direction: column;
  height: 100vh;
  background: #f0f2f5;
}

.navbar {
  display: flex;
  align-items: center;
  height: 56px;
  padding: 0 20px;
  background: #1a1a2e;
  color: #fff;
}

.logo {
  font-size: 18px;
  font-weight: bold;
}

.chat-area {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
}

.input-bar {
  display: flex;
  gap: 10px;
  padding: 16px 20px;
  background: #fff;
  border-top: 1px solid #e0e0e0;
}

.input {
  flex: 1;
  padding: 12px 16px;
  border: 1px solid #ddd;
  border-radius: 8px;
  font-size: 15px;
  outline: none;
}

.input:focus {
  border-color: #4a90d9;
}

.send-btn {
  padding: 12px 24px;
  background: #4a90d9;
  color: #fff;
  border: none;
  border-radius: 8px;
  font-size: 15px;
  cursor: pointer;
}

.send-btn:hover {
  background: #357abd;
}
</style>

保存,去浏览器看看效果。你应该能看到一个深色顶栏、灰色消息区、白色底部输入栏的三段式布局。


5.3 创建组件

现在我们把各个部分拆成独立组件。在 src/components/ 下创建以下 4 个文件。

<template>
  <header class="navbar">
    <span class="logo">🤖 AI Chat</span>
    <span class="subtitle">你的 AI 助手</span>
  </header>
</template>

<script setup>
</script>

<style scoped>
.navbar {
  display: flex;
  align-items: center;
  gap: 12px;
  height: 56px;
  padding: 0 20px;
  background: #1a1a2e;
  color: #fff;
}

.logo {
  font-size: 18px;
  font-weight: bold;
}

.subtitle {
  font-size: 13px;
  color: #8888aa;
}
</style>

MessageBubble.vue

这是单条消息气泡组件,根据角色(用户/AI)显示不同样式:

<template>
  <div class="message" :class="role">
    <div class="avatar">{{ role === 'user' ? '🧑' : '🤖' }}</div>
    <div class="bubble">{{ content }}</div>
  </div>
</template>

<script setup>
defineProps({
  content: String,
  role: String   // 'user' 或 'assistant'
})
</script>

<style scoped>
.message {
  display: flex;
  gap: 10px;
  margin-bottom: 16px;
  max-width: 80%;
}

.message.user {
  flex-direction: row-reverse;
  margin-left: auto;
}

.message.assistant {
  margin-right: auto;
}

.avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: #e0e0e0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 18px;
  flex-shrink: 0;
}

.message.user .avatar {
  background: #4a90d9;
}

.bubble {
  padding: 12px 16px;
  border-radius: 12px;
  font-size: 15px;
  line-height: 1.6;
  word-break: break-word;
  white-space: pre-wrap;
}

.message.user .bubble {
  background: #4a90d9;
  color: #fff;
  border-top-right-radius: 4px;
}

.message.assistant .bubble {
  background: #fff;
  color: #333;
  border-top-left-radius: 4px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
</style>

ChatArea.vue

消息列表区域,循环渲染所有消息气泡,并支持自动滚动到底部:

<template>
  <main class="chat-area" ref="chatRef">
    <div v-if="messages.length === 0" class="empty-tip">
      发送一条消息开始对话吧 💬
    </div>

    <MessageBubble
      v-for="(msg, index) in messages"
      :key="index"
      :content="msg.content"
      :role="msg.role"
    />

    <div v-if="loading" class="loading">
      <span class="dot-animation">AI 正在思考</span>
    </div>
  </main>
</template>

<script setup>
import { ref, watch, nextTick } from 'vue'
import MessageBubble from './MessageBubble.vue'

const props = defineProps({
  messages: Array,
  loading: Boolean
})

const chatRef = ref(null)

// 每当消息列表变化,自动滚动到底部
watch(
  () => props.messages.length,
  async () => {
    await nextTick()
    if (chatRef.value) {
      chatRef.value.scrollTop = chatRef.value.scrollHeight
    }
  }
)
</script>

<style scoped>
.chat-area {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
}

.empty-tip {
  text-align: center;
  color: #999;
  margin-top: 40vh;
  font-size: 16px;
}

.loading {
  text-align: center;
  color: #888;
  padding: 10px;
}

.dot-animation::after {
  content: '';
  animation: dots 1.5s infinite;
}

@keyframes dots {
  0%   { content: ''; }
  25%  { content: '.'; }
  50%  { content: '..'; }
  75%  { content: '...'; }
}
</style>

InputBar.vue

底部输入栏,支持点击发送和回车发送:

<template>
  <footer class="input-bar">
    <input
      class="input"
      v-model="inputText"
      placeholder="输入你的问题..."
      @keyup.enter="handleSend"
    />
    <button class="send-btn" @click="handleSend" :disabled="!inputText.trim()">
      发送
    </button>
  </footer>
</template>

<script setup>
import { ref } from 'vue'

const emit = defineEmits(['send'])
const inputText = ref('')

function handleSend() {
  const text = inputText.value.trim()
  if (!text) return

  emit('send', text)
  inputText.value = ''
}
</script>

<style scoped>
.input-bar {
  display: flex;
  gap: 10px;
  padding: 16px 20px;
  background: #fff;
  border-top: 1px solid #e0e0e0;
}

.input {
  flex: 1;
  padding: 12px 16px;
  border: 1px solid #ddd;
  border-radius: 8px;
  font-size: 15px;
  outline: none;
  transition: border-color 0.2s;
}

.input:focus {
  border-color: #4a90d9;
}

.send-btn {
  padding: 12px 24px;
  background: #4a90d9;
  color: #fff;
  border: none;
  border-radius: 8px;
  font-size: 15px;
  cursor: pointer;
  transition: background 0.2s;
}

.send-btn:hover:not(:disabled) {
  background: #357abd;
}

.send-btn:disabled {
  background: #a0c4e8;
  cursor: not-allowed;
}
</style>

5.4 组装与核心逻辑

组件都准备好了,现在回到 App.vue,把它们组装起来。

先实现一个”模拟版”,AI 回复用 setTimeout 模拟延迟,确保整个流程跑通:

<template>
  <div class="app">
    <NavBar />
    <ChatArea :messages="messages" :loading="loading" />
    <InputBar @send="handleSend" />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import NavBar from './components/NavBar.vue'
import ChatArea from './components/ChatArea.vue'
import InputBar from './components/InputBar.vue'

const messages = ref([])
const loading = ref(false)

function handleSend(text) {
  // 1. 添加用户消息
  messages.value.push({
    role: 'user',
    content: text
  })

  // 2. 显示 loading
  loading.value = true

  // 3. 模拟 AI 回复(延迟 1 秒)
  setTimeout(() => {
    messages.value.push({
      role: 'assistant',
      content: `你说的是:"${text}"。\n\n这是一条模拟的 AI 回复,后面我们会对接真实的 AI 接口。`
    })
    loading.value = false
  }, 1000)
}
</script>

<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.app {
  display: flex;
  flex-direction: column;
  height: 100vh;
  background: #f0f2f5;
}
</style>

保存所有文件,去浏览器试试!输入一条消息点发送,应该能看到:

  1. 你的消息出现在右边(蓝色气泡)
  2. “AI 正在思考” 闪烁 1 秒
  3. AI 的回复出现在左边(白色气泡)

如果效果对了,恭喜你,核心功能已经跑通了!


5.5 对接真实 AI 接口

模拟版玩够了,现在对接真实的 AI API。我们使用 OpenAI 兼容格式的接口(国内大部分 AI API 都兼容这个格式)。

修改 App.vue<script setup> 部分:

<script setup>
import { ref } from 'vue'
import NavBar from './components/NavBar.vue'
import ChatArea from './components/ChatArea.vue'
import InputBar from './components/InputBar.vue'

const messages = ref([])
const loading = ref(false)

// ⚠️ 替换成你自己的 API 地址和 Key
const API_URL = 'https://api.example.com/v1/chat/completions'
const API_KEY = 'sk-your-api-key-here'

async function sendToAI(chatMessages) {
  const res = await fetch(API_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}`
    },
    body: JSON.stringify({
      model: 'gpt-3.5-turbo',
      messages: chatMessages
    })
  })

  if (!res.ok) {
    throw new Error(`请求失败:${res.status}`)
  }

  const data = await res.json()
  return data.choices[0].message.content
}

async function handleSend(text) {
  // 1. 添加用户消息
  messages.value.push({
    role: 'user',
    content: text
  })

  // 2. 构建发送给 API 的消息列表(包含历史对话,实现多轮记忆)
  const chatMessages = messages.value.map(msg => ({
    role: msg.role,
    content: msg.content
  }))

  // 3. 调用 AI 接口
  loading.value = true
  try {
    const reply = await sendToAI(chatMessages)
    messages.value.push({
      role: 'assistant',
      content: reply
    })
  } catch (error) {
    messages.value.push({
      role: 'assistant',
      content: `❌ 出错了:${error.message}\n请检查 API 地址和密钥是否正确。`
    })
  } finally {
    loading.value = false
  }
}
</script>

重要提示:

  • API_URLAPI_KEY 需要替换成你自己的。课程中老师会提供。
  • 在正式项目中,API Key 不应该写在前端代码里(会被用户看到),应该放在后端。这里为了教学方便,先直接写在前端。
  • chatMessages 包含了所有历史消息,这样 AI 就能”记住”之前的对话内容。

5.6 样式美化

我们来把界面打磨得更好看一些。更新各组件的样式。

App.vue 添加一些全局样式优化:

<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* 美化滚动条 */
.chat-area::-webkit-scrollbar {
  width: 6px;
}

.chat-area::-webkit-scrollbar-thumb {
  background: #ccc;
  border-radius: 3px;
}

.chat-area::-webkit-scrollbar-thumb:hover {
  background: #aaa;
}

.app {
  display: flex;
  flex-direction: column;
  height: 100vh;
  background: #f0f2f5;
  max-width: 800px;
  margin: 0 auto;
  box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}

/* 手机端适配 */
@media (max-width: 800px) {
  .app {
    max-width: 100%;
    box-shadow: none;
  }
}
</style>

加了 max-width: 800pxmargin: 0 auto,页面在大屏幕上会居中显示,像一个聊天窗口。手机上则自动撑满。


5.7 进阶优化(选学)

以下内容是加分项,有兴趣的同学可以挑战。

流式响应(打字机效果)

让 AI 的回复像打字一样一个字一个字蹦出来,而不是等全部生成完才显示。

核心思路:请求时加上 stream: true,然后用 ReadableStream 逐块读取。

async function sendToAIStream(chatMessages) {
  const res = await fetch(API_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}`
    },
    body: JSON.stringify({
      model: 'gpt-3.5-turbo',
      messages: chatMessages,
      stream: true
    })
  })

  // 先在消息列表中添加一条空的 AI 消息
  messages.value.push({ role: 'assistant', content: '' })
  const lastIndex = messages.value.length - 1

  const reader = res.body.getReader()
  const decoder = new TextDecoder()

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

    const chunk = decoder.decode(value)
    const lines = chunk.split('\n').filter(line => line.startsWith('data: '))

    for (const line of lines) {
      const data = line.slice(6) // 去掉 "data: "
      if (data === '[DONE]') return

      try {
        const json = JSON.parse(data)
        const text = json.choices[0].delta?.content || ''
        messages.value[lastIndex].content += text
      } catch (e) {
        // 跳过解析错误
      }
    }
  }
}

Markdown 渲染

AI 的回复通常包含代码块、列表等 Markdown 格式。安装 marked 库来渲染:

npm install marked

MessageBubble.vue 中使用:

<template>
  <div class="message" :class="role">
    <div class="avatar">{{ role === 'user' ? '🧑' : '🤖' }}</div>
    <div class="bubble" v-html="renderedContent"></div>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { marked } from 'marked'

const props = defineProps({
  content: String,
  role: String
})

const renderedContent = computed(() => {
  if (props.role === 'assistant') {
    return marked(props.content)
  }
  return props.content
})
</script>

本地存储

localStorage 保存聊天记录,刷新页面不丢失:

import { ref, watch } from 'vue'

// 从本地存储加载历史消息
const saved = localStorage.getItem('chat-messages')
const messages = ref(saved ? JSON.parse(saved) : [])

// 消息变化时自动保存
watch(messages, (newVal) => {
  localStorage.setItem('chat-messages', JSON.stringify(newVal))
}, { deep: true })

5.8 项目总结

恭喜你!你已经完成了一个完整的 AI 对话应用。回顾一下我们用到了什么:

技术用在哪里
HTML页面结构:导航栏、消息区、输入栏
CSS + Flex三段式布局、消息气泡样式、响应式适配
JavaScript数组操作、async/await、fetch API
Vue 组件NavBar、ChatArea、MessageBubble、InputBar
Vue 响应式ref 管理消息列表和 loading 状态
Vue 模板语法v-for 渲染列表、v-if 条件显示、v-model 输入绑定、@事件监听
Props / Emit组件之间的数据传递和事件通信

最终项目结构

ai-chat/
├── index.html
├── package.json
├── src/
│   ├── main.js
│   ├── App.vue
│   └── components/
│       ├── NavBar.vue
│       ├── ChatArea.vue
│       ├── MessageBubble.vue
│       └── InputBar.vue
└── public/

扩展挑战

如果你觉得不过瘾,试试这些:

  1. 新建对话 — 加一个”新对话”按钮,清空消息重新开始
  2. 对话历史 — 侧边栏显示历史对话列表,可以切换
  3. 主题切换 — 深色模式 / 浅色模式切换
  4. 头像自定义 — 让用户设置自己的头像
  5. 导出聊天记录 — 把对话导出为文本文件

你已经从零开始,掌握了 HTML、CSS、JavaScript、Vue 四项前端核心技能,并做出了一个真实可用的 AI 对话应用。 这些技能足以支撑你后续的 AI 应用开发。前端的世界很大,但你已经有了最坚实的基础,剩下的就是在实战中不断进步。加油!