手把手拆解:使用vue3打造超酷AI对话页面
一、HTML部分:搭建页面框架
咱们先从HTML部分开始看起,这就好比是搭房子,HTML搭建的就是整个页面的框架。
(一)整体布局容器
<template>
<div class="container">
<div class="bailian-demo">
这里最外层的<template>
标签,它是Vue.js特有的,用来包裹页面的模板内容。然后是<div class="container">
,这个container
类就像是一个大箱子,把整个页面的内容都装在里面,并且通过CSS设置了一些通用的样式,比如说最大宽度、外边距、内边距啥的,让页面内容在不同屏幕大小下都能有个合适的布局。再里面的<div class="bailian - demo">
,它是整个AI对话页面主体的容器,设定了一些样式来确定它的整体外观,比如背景颜色、边框圆角、阴影等等,让页面看起来更美观、更有质感。
(二)左侧历史记录面板
<div class="history-panel" :class="{ 'history-collapsed': isHistoryCollapsed }">
<div class="history-header">
<h3>对话历史</h3>
<button class="toggle-history" @click="toggleHistory">
<i class="icon-collapse"></i>
{{ isHistoryCollapsed? '展开' : '收起' }}
</button>
</div>
<div class="history-list">
<div
v-for="(item, index) in history"
:key="index"
class="history-item"
@click="loadHistoryItem(index)"
:class="{ 'active-history': activeHistoryIndex === index }"
>
<div class="history-question">{{ truncateText(item.question) }}</div>
<div class="history-time">{{ formatTime(item.time) }}</div>
</div>
<div v-if="history.length === 0" class="empty-history">
<i class="icon-history"></i>
<p>暂无历史记录</p>
</div>
</div>
</div>
这一大块就是左侧的历史记录面板啦。<div class="history-panel" :class="{ 'history - collapsed': isHistoryCollapsed }">
,这里的history - panel
类定义了这个面板的基本样式,像宽度、背景渐变、边框等等。而后面的:class
绑定是Vue.js的语法,它会根据isHistoryCollapsed
这个变量的值来动态添加或移除history - collapsed
类。啥意思呢?就是说如果isHistoryCollapsed
为true
,就会添加history - collapsed
类,这样面板就能实现收起的效果啦,反之则是展开状态。
再看里面的<div class="history - header">
,这是历史记录面板的头部,包含了一个标题<h3>对话历史</h3>
,让用户一眼就知道这是干啥的。还有一个按钮<button class="toggle - history" @click="toggleHistory">
,这个按钮可重要啦,@click="toggleHistory"
表示当用户点击这个按钮时,会触发Vue.js组件里定义的toggleHistory
函数,这个函数就是用来切换历史记录面板展开或收起状态的。按钮里面有个小图标<i class="icon - collapse"></i>
,还有一段文字{{ isHistoryCollapsed? '展开' : '收起' }}
,这段文字也是根据isHistoryCollapsed
的值动态显示的,很智能吧!
接下来是<div class="history - list">
,这里面放的就是具体的历史记录列表啦。通过v - for
指令,也就是<div v - for="(item, index) in history" :key="index" class="history - item" @click="loadHistoryItem(index)" :class="{ 'active - history': activeHistoryIndex === index }">
,它会循环遍历history
数组,这个数组里面存的就是所有的历史对话记录。每一条记录都会生成一个<div class="history - item">
,这里的history - item
类定义了每个历史记录项的样式,像背景颜色、边框半径、鼠标悬停效果等等。@click="loadHistoryItem(index)"
表示当用户点击某条历史记录时,会触发loadHistoryItem
函数,并且把这条记录的索引index
传进去,这样就能加载这条历史记录的内容到输入框和结果展示区域啦。:class="{ 'active - history': activeHistoryIndex === index }"
这个绑定呢,会根据activeHistoryIndex
这个变量的值来判断当前这条记录是否是激活状态,如果是,就会添加active - history
类,给这条记录加上特殊的样式,比如改变背景颜色啥的,让用户知道当前选中的是哪条记录。
每个历史记录项里面又有两个小的<div>
,<div class="history - question">{{ truncateText(item.question) }}</div>
用来显示问题内容,这里的truncateText
函数会把问题内容截断显示,防止太长了影响页面布局。<div class="history - time">{{ formatTime(item.time) }}</div>
则是用来显示这条记录的时间,formatTime
函数会把时间戳格式化成我们常见的日期时间格式。
最后还有一个<div v - if="history.length === 0" class="empty - history">
,当history
数组长度为0,也就是没有历史记录的时候,就会显示这个<div>
,里面有个小图标和一段文字提示用户“暂无历史记录”。
(三)历史记录展开按钮(当收起时显示)
<button class="expand - history - btn" @click="toggleHistory" v - if="isHistoryCollapsed">
<i class="icon - expand"></i>
</button>
这个按钮很简单,当历史记录面板处于收起状态,也就是isHistoryCollapsed
为true
的时候,它就会显示出来。同样,点击这个按钮会触发toggleHistory
函数,用来展开历史记录面板。按钮里面有个<i class="icon - expand"></i>
小图标,告诉用户点击这里可以展开历史记录。
(四)主内容区域
<div class="main - content" :class="{ 'content - expanded': isHistoryCollapsed }">
<div class="header">
<h1><span style="cursor: pointer;color: #641acf" @click="router.push('/')">月木</span>AI助手</h1>
<p class="subtitle">智能AI对话体验</p>
</div>
<div class="content - area">
<div class="result - section" v - if="result || isLoading">
<div class="result - header">
<h3>AI回答</h3>
<button class="copy - button" @click="copyResult" :disabled="!result">
<i class="icon - copy"></i> 复制
</button>
</div>
<div class="markdown - content" ref="resultContainer" v - html="renderedResult"></div>
</div>
<div class="error - message" v - if="error">
<i class="icon - error"></i> {{ error }}
</div>
<transition name="fade">
<div class="copy - notification" v - if="showCopyNotification">
<i class="icon - check"></i> 复制成功
</div>
</transition>
</div>
<div class="input - container">
<div class="input - section">
<textarea
v - model="prompt"
placeholder="输入你的问题..."
@keydown.enter.exact.prevent="generateText"
@keydown.shift.enter="handleShiftEnter"
></textarea>
<div class="button - container">
<div class="hint">按Enter发送,Shift+Enter换行</div>
<button @click="generateText" :disabled="isLoading">
<span v - if="isLoading">
<span class="spinner"></span> 生成中...
</span>
<span v - else>发送 <i class="icon - send"></i></span>
</button>
</div>
</div>
</div>
</div>
这一大块就是主内容区域啦。<div class="main - content" :class="{ 'content - expanded': isHistoryCollapsed }">
,main - content
类定义了主内容区域的基本样式,:class
绑定也是根据isHistoryCollapsed
的值来动态添加或移除content - expanded
类,当历史记录面板收起时,主内容区域会有一些样式上的变化,比如可能会占据更多的屏幕宽度啥的。
再看里面的<div class="header">
,这是主内容区域的头部,有一个大标题<h1><span style="cursor: pointer;color: #641acf" @click="router.push('/')">月木</span>AI助手</h1>
,这个标题里面的“月木”两个字是有链接效果的,当用户点击时,会触发router.push('/')
,这是Vue Router的语法,会把用户导航到根路径。旁边还有一个副标题<p class="subtitle">智能AI对话体验</p>
,简单介绍了这个AI助手的功能。
接下来是<div class="content - area">
,这里面放的就是主要的内容展示区域啦。<div class="result - section" v - if="result || isLoading">
,这个result - section
会在result
有值(也就是AI有回答了)或者isLoading
为true
(也就是正在加载AI回答)的时候显示出来。<div class="result - header">
是结果区域的头部,有一个标题<h3>AI回答</h3>
,还有一个复制按钮<button class="copy - button" @click="copyResult" :disabled="!result">
,@click="copyResult"
表示点击这个按钮会触发copyResult
函数,用来复制AI的回答内容。:disabled="!result"
表示当result
没有值的时候,也就是AI还没有回答时,这个按钮是禁用状态,用户不能点击。按钮里面有个小图标<i class="icon - copy"></i>
和文字“复制”。
再下面的<div class="markdown - content" ref="resultContainer" v - html="renderedResult"></div>
,这里的markdown - content
类定义了显示结果的样式,ref="resultContainer"
给这个<div>
起了个引用名,方便在JavaScript代码里获取这个元素。v - html="renderedResult"
则是把renderedResult
这个变量的值以HTML的形式渲染到这个<div>
里面,renderedResult
是通过计算属性把Markdown格式的结果转换为HTML格式的,后面我们在JavaScript部分会详细讲到。
然后是<div class="error - message" v - if="error">
,当error
变量有值的时候,也就是在请求AI接口出现错误的时候,这个<div>
就会显示出来,里面有个小图标<i class="icon - error"></i>
和错误信息{{ error }}
,告诉用户哪里出问题了。
还有一个<transition name="fade">
包裹的<div class="copy - notification" v - if="showCopyNotification">
,当showCopyNotification
为true
,也就是用户成功复制AI回答内容后,这个提示框会显示出来,显示“复制成功”的提示信息,并且有一个淡入淡出的动画效果,这个动画效果是通过CSS的fade
类来实现的。
最后是<div class="input - container">
,这是输入框区域啦。<div class="input - section">
里面有一个<textarea>
输入框,v - model="prompt"
表示这个输入框绑定了prompt
变量,用户在输入框里输入的内容会实时同步到prompt
变量里。placeholder="输入你的问题..."
是输入框的占位提示文字。@keydown.enter.exact.prevent="generateText"
表示当用户按下回车键(并且是直接按回车键,不是和其他键组合)时,会触发generateText
函数,这个函数就是用来向AI发送请求,生成回答的。@keydown.shift.enter="handleShiftEnter"
表示当用户按下Shift + Enter
组合键时,会触发handleShiftEnter
函数,这个函数用来在输入框里实现换行功能。
输入框下面是<div class="button - container">
,里面有一个提示文字<div class="hint">按Enter发送,Shift+Enter换行</div>
,告诉用户输入框的操作方式。还有一个发送按钮<button @click="generateText" :disabled="isLoading">
,这个按钮和输入框的回车键功能一样,点击会触发generateText
函数,:disabled="isLoading"
表示当正在加载AI回答时,这个按钮是禁用状态,防止用户重复点击。按钮里面根据isLoading
的值显示不同的内容,正在加载时显示一个加载动画<span v - if="isLoading"><span class="spinner"></span> 生成中...</span>
,加载完成后显示“发送 ”。
二、JavaScript部分:赋予页面交互和功能
HTML部分搭好了框架,接下来就是JavaScript部分给这个页面赋予生命啦,让它能响应用户的操作,实现各种功能。
(一)引入必要的模块和设置
import {useRouter} from 'vue - router'
import {computed, nextTick, onMounted, ref} from 'vue'
import OpenAI from 'openai'
import {marked} from'marked'
import hljs from 'highlight.js'
import 'highlight.js/styles/github.css'
const router = useRouter()
onMounted(() => {
// 从localStorage加载历史记录
const savedHistory = localStorage.getItem('bailianHistory')
if (savedHistory) {
history.value = JSON.parse(savedHistory)
}
})
一开始,我们引入了好多东西。import {useRouter} from 'vue - router'
引入了Vue Router的useRouter
函数,这个函数用来在组件里使用路由功能,还记得我们在HTML里点击“月木”跳转到根路径的功能吧,就是靠它实现的。
import {computed, nextTick, onMounted, ref} from 'vue'
引入了Vue.js的一些核心功能。ref
用来创建响应式变量,比如说我们后面会用到的prompt
(用户输入的问题)、result
(AI的回答)、error
(错误信息)等等。computed
用来创建计算属性,我们后面会有一个计算属性renderedResult
,它会根据result
的值动态生成HTML格式的内容。nextTick
函数可以在DOM更新之后执行回调函数,我们在更新AI回答内容后,需要滚动到最底部显示最新内容,就会用到它。onMounted
这个钩子函数会在组件挂载到DOM之后执行,我们在这里从localStorage
加载历史记录,localStorage
是浏览器提供的一种本地存储机制,可以在浏览器端存储一些数据,并且在页面刷新或者关闭后数据还能保留。我们从里面获取bailianHistory
这个键对应的值,如果有值,就把它解析成JSON格式,然后赋值给history
变量,这个history
变量就是用来存储历史对话记录的。
import OpenAI from 'openai'
引入了OpenAI的JavaScript库,我们要用它来调用OpenAI的API,让AI给我们生成回答。import {marked} from'marked'
和import hljs from 'highlight.js'
以及import 'highlight.js/styles/github.css'
这几句是用来处理Markdown格式文本的。marked
是一个Markdown解析器,我们可以把Markdown格式的文本转换成HTML格式。highlight.js
是一个代码高亮库,我们可以用它来给代码块添加漂亮的高亮样式,后面我们会自定义marked
的渲染器,让它能识别代码块并且使用highlight.js
进行高亮处理。
(二)定义响应式变量
//提示词
const prompt = ref('')
//结果
const result = ref('')
//页面错误提示
const error = ref(null)
//是否加载中
const isLoading = ref(false)
// 结果容器元素
const resultContainer = ref(null)
// 当前激活的历史记录索引
const activeHistoryIndex = ref(-1)
// 历史记录
const history = ref([])
// 复制成功提示
const showCopyNotification = ref(false)
这里定义了一堆响应式变量,这些变量就像是页面的“小管家”,它们的值一变,页面上对应的部分也会跟着变。prompt
用来存储用户在输入框里输入的问题,一开始是空字符串。result
用来存储AI返回的回答,一开始也是空的。error
用来存储请求AI接口时出现的错误信息,一开始是null
,表示没有错误。isLoading
用来表示当前是否正在加载AI的回答,一开始是false
,表示没有在加载。resultContainer
是用来引用显示AI回答结果的那个<div>
元素的,一开始是null
,等组件挂载后会被赋值。activeHistoryIndex
用来记录当前激活的历史记录的索引,一开始是`-