手把手拆解:使用vue3打造超酷AI对话页面

手把手拆解:使用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类。啥意思呢?就是说如果isHistoryCollapsedtrue,就会添加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>

这个按钮很简单,当历史记录面板处于收起状态,也就是isHistoryCollapsedtrue的时候,它就会显示出来。同样,点击这个按钮会触发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有回答了)或者isLoadingtrue(也就是正在加载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">,当showCopyNotificationtrue,也就是用户成功复制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用来记录当前激活的历史记录的索引,一开始是`-

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

月木@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值