第五章:综合实战 — 用 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 个文件。
NavBar.vue
<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>
保存所有文件,去浏览器试试!输入一条消息点发送,应该能看到:
- 你的消息出现在右边(蓝色气泡)
- “AI 正在思考” 闪烁 1 秒
- 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_URL和API_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: 800px和margin: 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/
扩展挑战
如果你觉得不过瘾,试试这些:
- 新建对话 — 加一个”新对话”按钮,清空消息重新开始
- 对话历史 — 侧边栏显示历史对话列表,可以切换
- 主题切换 — 深色模式 / 浅色模式切换
- 头像自定义 — 让用户设置自己的头像
- 导出聊天记录 — 把对话导出为文本文件
你已经从零开始,掌握了 HTML、CSS、JavaScript、Vue 四项前端核心技能,并做出了一个真实可用的 AI 对话应用。 这些技能足以支撑你后续的 AI 应用开发。前端的世界很大,但你已经有了最坚实的基础,剩下的就是在实战中不断进步。加油!