feat(layout): 添加基础布局组件和侧边栏功能- 新增 BasicLayout.vue 基础布局组件,包含侧边栏和主内容区域- 实现侧边栏展开/收缩功能,支持通过 props 控制显示状态- 添加侧边栏切换按钮,支持点击切换显示状态- 新增 SideBar.vue 组件,包含历史对话列表和新对话按钮
- 实现历史对话项的展示和操作菜单(重命名、删除) - 添加 AI 机器人 logo 和新对话图标 SVG 文件 - 引入 ant-design-vue 图标组件库,支持菜单操作图标 - 更新 IndexPage.vue 使用新的布局组件重构页面结构 - 移除原有聊天界面相关逻辑,简化首页展示内容- 添加底部提示文字"内容由 AI 生成,请仔细甄别"
This commit is contained in:
5
components.d.ts
vendored
5
components.d.ts
vendored
@@ -12,8 +12,13 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AButton: typeof import('ant-design-vue/es')['Button']
|
||||
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
|
||||
AMenu: typeof import('ant-design-vue/es')['Menu']
|
||||
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SideBar: typeof import('./src/components/SideBar.vue')['default']
|
||||
StreamMarkdownRender: typeof import('./src/components/StreamMarkdownRender.vue')['default']
|
||||
SvgIcon: typeof import('./src/components/SvgIcon.vue')['default']
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"@tailwindcss/vite": "^4.1.15",
|
||||
"ant-design-vue": "~4.2.6",
|
||||
"highlight.js": "^11.11.1",
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@ant-design/icons-vue':
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1(vue@3.5.22(typescript@5.9.3))
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.15
|
||||
version: 4.1.15(vite@7.1.11(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2))
|
||||
|
||||
6
src/assets/icons/ai-robot-logo.svg
Normal file
6
src/assets/icons/ai-robot-logo.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg t="1755328337279" fill="currentColor" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="35341" width="200" height="200">
|
||||
<path
|
||||
d="M585.152 85.312C827.52 85.312 1024 276.352 1024 512s-196.48 426.688-438.848 426.688H438.848C196.48 938.688 0 747.648 0 512s196.48-426.688 438.848-426.688h146.304z m103.936 244.48H334.912C231.424 329.792 147.52 411.392 147.52 512c0 99.648 82.24 180.608 184.32 182.208h357.248c103.488 0 187.392-81.6 187.392-182.208s-83.84-182.208-187.392-182.208zM347.392 418.688c37.184 0 67.328 29.312 67.328 65.536v56.64c0 36.224-30.08 65.6-67.328 65.6-37.184 0-67.328-29.376-67.328-65.6v-56.64c0-36.224 30.08-65.536 67.328-65.536z m415.424 9.088a35.648 35.648 0 0 1-11.136 49.6l-0.512 0.32-52.928 32.448 51.008 27.264a35.712 35.712 0 0 1 14.976 48.64l-0.256 0.512a37.696 37.696 0 0 1-49.92 14.592l-0.576-0.32L607.168 544a35.712 35.712 0 0 1-2.624-61.888l0.704-0.448 106.304-65.216a37.76 37.76 0 0 1 51.264 11.328z"
|
||||
fill="#165DFF" p-id="35342"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
6
src/assets/icons/new-chat.svg
Normal file
6
src/assets/icons/new-chat.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg t="1755332748552" fill="currentColor" viewBox="0 0 1109 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="37243" width="200" height="200">
|
||||
<path
|
||||
d="M1018.154667 82.56C1080.32 145.066667 1088 202.581333 1088 449.493333c0 246.698667-7.68 304.512-69.845333 366.933334-60.16 60.586667-115.968 69.802667-342.784 70.357333h-22.442667c-47.36 0-105.301333 18.346667-169.216 51.797333-149.973333 78.208-171.946667 86.784-214.442667 58.026667-32.213333-21.76-40.106667-40.96-35.84-117.674667-63.402667-8.874667-107.392-27.392-142.250666-62.464C31.018667 756.053333 21.888 699.989333 21.333333 472.149333v-22.613333c0-246.698667 7.68-304.512 69.845334-366.933333C151.338667 21.973333 207.146667 12.8 433.92 12.202667h218.965333c245.376 0 303.018667 7.808 365.226667 70.314666z m-344.746667 13.909333h-237.482667c-200.106667 0.469333-247.722667 7.936-285.013333 45.482667-37.418667 37.589333-44.928 85.76-45.354667 286.933333v41.258667c0.469333 201.386667 7.936 249.386667 45.312 287.018667 19.456 19.498667 45.824 31.104 89.002667 37.632l0.64-4.949334 2.090667-22.997333a42.112 42.112 0 0 1 32.213333-37.205333l6.570667-1.024h6.912a42.112 42.112 0 0 1 38.144 45.738666l-2.090667 23.04c-6.613333 72.618667-7.850667 108.458667-8.021333 122.282667v4.693333l0.085333 2.474667c-0.256-0.128 32.768-13.056 128.341333-62.933333 74.965333-39.125333 144.853333-61.354667 208.170667-61.354667 217.472 0 267.008-6.741333 305.493333-45.482667 38.613333-38.826667 45.354667-88.96 45.354667-307.541333 0-218.837333-6.656-268.8-45.312-307.626667-37.333333-37.461333-85.077333-44.970667-285.056-45.44zM554.666667 259.285333a42.112 42.112 0 0 1 42.112 42.069334l-0.085334 105.984 105.344 0.085333a42.112 42.112 0 0 1 41.557334 35.285333l0.554666 6.826667a42.112 42.112 0 0 1-42.112 42.069333h-105.344l0.085334 106.069334a42.069333 42.069333 0 0 1-35.328 41.514666l-6.784 0.64a42.112 42.112 0 0 1-42.112-42.112l-0.085334-106.154666-105.173333 0.042666a42.112 42.112 0 0 1-41.557333-35.285333l-0.554667-6.826667a42.112 42.112 0 0 1 42.112-42.069333h105.216l0.042667-106.197333a42.069333 42.069333 0 0 1 35.328-41.557334l6.784-0.426666z"
|
||||
p-id="37244"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
1
src/assets/icons/sidebar-close.svg
Normal file
1
src/assets/icons/sidebar-close.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1755325907291" fill="currentColor" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="28009" width="200" height="200"><path d="M638.763 970.667h-256c-231.766 0-330.667-98.902-330.667-330.667V384c0-231.765 98.901-330.667 330.667-330.667h256c231.765 0 330.666 98.902 330.666 330.667v256c0 231.765-98.901 330.667-330.666 330.667z m-256-853.334c-196.864 0-266.667 69.803-266.667 266.667v256c0 196.864 69.803 266.667 266.667 266.667h256c196.864 0 266.666-69.803 266.666-266.667V384c0-196.864-69.76-266.667-266.666-266.667z" p-id="28010"></path><path d="M606.763 938.667V85.333a32.213 32.213 0 0 1 32-32 32.213 32.213 0 0 1 32 32v853.334a32 32 0 0 1-64 0z m-289.28-294.742a32.17 32.17 0 0 1 0-45.226l86.613-86.614-86.613-86.698a32 32 0 0 1 45.184-45.227l109.226 109.227a32.17 32.17 0 0 1 0 45.226L362.667 643.925a30.293 30.293 0 0 1-22.614 9.387 31.659 31.659 0 0 1-22.57-9.387z" p-id="28011"></path></svg>
|
||||
|
After Width: | Height: | Size: 937 B |
1
src/assets/icons/sidebar-open.svg
Normal file
1
src/assets/icons/sidebar-open.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1755324148406" fill="currentColor" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="26171" width="200" height="200"><path d="M768 0H256C115.2 0 0 115.2 0 256v512c0 140.8 115.2 256 256 256h512c140.8 0 256-115.2 256-256V256c0-140.8-115.2-256-256-256zM119.466667 904.533333c-38.4-38.4-55.466667-81.066667-55.466667-136.533333V256c0-51.2 17.066667-98.133333 55.466667-136.533333 38.4-38.4 85.333333-55.466667 136.533333-55.466667h21.333333v896H256c-51.2 0-98.133333-17.066667-136.533333-55.466667zM960 768c0 51.2-17.066667 98.133333-55.466667 136.533333-38.4 38.4-81.066667 55.466667-136.533333 55.466667H341.333333v-896h426.666667c51.2 0 98.133333 17.066667 136.533333 55.466667 38.4 38.4 55.466667 81.066667 55.466667 136.533333v512z" p-id="26172"></path><path d="M699.733333 618.666667l-106.666666-102.4 106.666666-110.933334c12.8-12.8 12.8-34.133333 0-46.933333-12.8-12.8-34.133333-12.8-46.933333 0l-128 132.266667c-12.8 12.8-12.8 34.133333 0 46.933333l128 123.733333c12.8 12.8 34.133333 12.8 46.933333 0 12.8-8.533333 12.8-29.866667 0-42.666666z" p-id="26173"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
141
src/components/SideBar.vue
Normal file
141
src/components/SideBar.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<!-- 左边栏 -->
|
||||
<div
|
||||
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'"
|
||||
class="w-64 bg-[#f9fbff] border-r border-gray-200 fixed left-0 top-0 h-full transition-transform duration-300 ease-in-out z-10 overflow-y-auto"
|
||||
>
|
||||
<!-- 侧边栏内容区域 -->
|
||||
<div class="p-0 h-full flex flex-col">
|
||||
<!-- Logo 与应用名称 -->
|
||||
<div @click="jumpToIndexPage" class="flex items-center justify-center p-4 cursor-pointer">
|
||||
<SvgIcon name="ai-robot-logo" customCss="w-8 h-8 text-gray-700 mr-3" />
|
||||
<span class="text-2xl font-bold font-sans tracking-wide text-gray-800">小维AI机器人</span>
|
||||
</div>
|
||||
|
||||
<!-- 开启新对话按钮 -->
|
||||
<button
|
||||
@click="jumpToIndexPage"
|
||||
class="mx-auto mb-[34px] my-2 px-6 py-2 text-white rounded-xl transition-colors new-chat-btn w-fit cursor-pointer"
|
||||
>
|
||||
<SvgIcon name="new-chat" customCss="w-6 h-6 mr-1.5 inline text-[#4d6bfe]" />
|
||||
开启新对话
|
||||
</button>
|
||||
|
||||
<!-- 历史对话区域 -->
|
||||
<div class="my-4 px-2 overflow-y-auto overflow-x-hidden flex-1">
|
||||
<div class="space-y-1">
|
||||
<div class="text-xs px-3 py-1 text-gray-500">历史对话</div>
|
||||
<div
|
||||
v-for="(historyChat, index) in historyChats"
|
||||
:key="index"
|
||||
class="relative px-3 py-1 rounded-xl hover:bg-[rgb(239,246,255)] cursor-pointer transition-colors flex items-center justify-between"
|
||||
@mouseenter="showButton = historyChat.uuid"
|
||||
@mouseleave="showButton = null"
|
||||
>
|
||||
<a-tooltip placement="top">
|
||||
<!-- Tooltip 提示文字 -->
|
||||
<template #title>
|
||||
<span>{{ historyChat.summary }}</span>
|
||||
</template>
|
||||
|
||||
<p class="text-[14px] text-gray-800 overflow-hidden whitespace-nowrap">
|
||||
{{ historyChat.summary }}
|
||||
</p>
|
||||
</a-tooltip>
|
||||
<!-- 下拉菜单 -->
|
||||
<a-dropdown>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="rename">
|
||||
<EditOutlined />
|
||||
重命名
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delete" danger>
|
||||
<DeleteOutlined />
|
||||
删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<!-- 右边菜单按钮 -->
|
||||
<button
|
||||
class="z-10 rounded-lg outline-none justify-center items-center bg-white w-6 h-6 flex absolute right-2 top-1/2 transform -translate-y-1/2 transition-all duration-300 hover:bg-gray-50"
|
||||
:style="{ opacity: showButton === historyChat.uuid ? 1 : 0 }"
|
||||
>
|
||||
<EllipsisOutlined class="w-4 h-4 text-gray-500" />
|
||||
</button>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 侧边栏切换按钮 -->
|
||||
<a-tooltip placement="bottom">
|
||||
<!-- Tooltip 提示文字 -->
|
||||
<template #title>
|
||||
<span>{{ sidebarOpen ? '收缩边栏' : '打开边栏' }}</span>
|
||||
</template>
|
||||
|
||||
<button
|
||||
:class="sidebarOpen ? 'left-64' : 'left-0'"
|
||||
@click="toggleSidebar"
|
||||
class="fixed top-4 z-20 bg-white border border-gray-200 rounded-r-lg p-2 transition-all duration-300"
|
||||
>
|
||||
<!-- 图标 -->
|
||||
<SvgIcon
|
||||
:name="sidebarOpen ? 'sidebar-open' : 'sidebar-close'"
|
||||
:customCss="sidebarOpen ? 'w-6 h-6 text-gray-400' : 'w-7 h-7 text-gray-400'"
|
||||
/>
|
||||
</button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
import { EditOutlined, EllipsisOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
const router = useRouter()
|
||||
// 跳转到首页
|
||||
const jumpToIndexPage = () => {
|
||||
router.push('/')
|
||||
}
|
||||
// 定义 props, 对外部暴露配置项
|
||||
defineProps<{
|
||||
sidebarOpen: boolean
|
||||
}>()
|
||||
|
||||
// 定义emits
|
||||
const emit = defineEmits(['toggle-sidebar'])
|
||||
|
||||
// 切换侧边栏显示/隐藏
|
||||
const toggleSidebar = () => {
|
||||
emit('toggle-sidebar')
|
||||
}
|
||||
|
||||
// 历史对话
|
||||
const historyChats = ref([
|
||||
{ uuid: '9640a419-4b0c-45dd-b16d-1980df2424c4', summary: '新对话1' },
|
||||
{ uuid: '7c2af48e-dce2-4822-aef6-c7a3c1949805', summary: '新对话2' },
|
||||
{ uuid: '152496bc-2776-422d-ac96-5dbfc903bc1d', summary: '新对话3' },
|
||||
])
|
||||
|
||||
// 当前显示右侧栏按钮的聊天 ID
|
||||
const showButton = ref<string | null>(null)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.overflow-y-auto {
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent; /* 自定义滚动条颜色 */
|
||||
}
|
||||
|
||||
.new-chat-btn {
|
||||
background-color: rgb(219 234 254);
|
||||
color: #4d6bfe;
|
||||
}
|
||||
|
||||
.new-chat-btn:hover {
|
||||
background-color: #c6dcf8;
|
||||
}
|
||||
</style>
|
||||
42
src/layouts/BasicLayout.vue
Normal file
42
src/layouts/BasicLayout.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import SideBar from '@/components/SideBar.vue'
|
||||
import { ref } from 'vue'
|
||||
// 左边栏状态,true 表示默认展开
|
||||
const sidebarOpen = ref(true)
|
||||
|
||||
// 定义props
|
||||
withDefaults(defineProps<{
|
||||
showFooterText?: boolean
|
||||
}>(), {
|
||||
showFooterText: true // 设置默认值
|
||||
})
|
||||
|
||||
// 切换侧边栏显示/隐藏
|
||||
const toggleSidebar = () => {
|
||||
sidebarOpen.value = !sidebarOpen.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-screen flex overflow-hidden overflow-x-hidden">
|
||||
<!-- 左边栏 -->
|
||||
<SideBar :sidebarOpen="sidebarOpen" @toggle-sidebar="toggleSidebar" />
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<div
|
||||
:class="sidebarOpen ? 'ml-64' : 'ml-0'"
|
||||
class="flex flex-col flex-1 transition-all duration-300"
|
||||
>
|
||||
<!-- 插槽 -->
|
||||
<slot name="main-content"></slot>
|
||||
</div>
|
||||
<!-- 吸附底部的提示文字 -->
|
||||
<div
|
||||
v-if="showFooterText"
|
||||
:class="sidebarOpen ? 'ml-64' : 'ml-0'"
|
||||
class="fixed bottom-0 left-0 right-0 flex items-center justify-center text-xs text-gray-400 transition-all duration-300 py-2"
|
||||
>
|
||||
内容由 AI 生成,请仔细甄别
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,233 +1,42 @@
|
||||
<template>
|
||||
<div class="h-screen flex flex-col overflow-y-auto" ref="chatContainer">
|
||||
<!-- 聊天记录区域 -->
|
||||
<div class="flex-1 max-w-3xl mx-auto pb-24 pt-4 px-4">
|
||||
<!-- 遍历聊天记录 -->
|
||||
<template v-for="(chat, index) in chatList" :key="index">
|
||||
<!-- 用户提问消息(靠右) -->
|
||||
<div v-if="chat.role === 'user'" class="flex justify-end mb-4">
|
||||
<div class="quesiton-container">
|
||||
<p>{{ chat.content }}</p>
|
||||
<Layout>
|
||||
<!-- 主内容区域 -->
|
||||
<template #main-content>
|
||||
<div class="flex items-center justify-center flex-1">
|
||||
<div class="max-w-3xl w-full">
|
||||
<div class="text-center mb-10">
|
||||
<div class="flex items-center justify-center mb-3">
|
||||
<SvgIcon name="ai-robot-logo" customCss="w-10 h-10 text-gray-700 mr-3" />
|
||||
<h2 class="text-2xl text-gray-800">我是小维 AI 机器人,很高兴见到你!</h2>
|
||||
</div>
|
||||
<p class="text-gray-500">我可以帮你写代码、写作各种创意内容,请把你的任务交给我吧~</p>
|
||||
</div>
|
||||
|
||||
<!-- 大模型回复消息(靠左) -->
|
||||
<div v-else class="flex mb-4">
|
||||
<!-- 头像 -->
|
||||
<div class="shrink-0 mr-3">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full flex items-center justify-center border border-gray-200"
|
||||
>
|
||||
<SvgIcon name="deepseek-logo" customCss="w-5 h-5"></SvgIcon>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 回复的内容 -->
|
||||
<div class="p-1 mb-2 max-w-[90%]">
|
||||
<StreamMarkdownRender :content="chat.content" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 提问输入框 -->
|
||||
<div class="sticky max-w-3xl mx-auto bg-white bottom-0 left-0 w-full">
|
||||
<div class="bg-gray-100 rounded-3xl px-4 py-3 mx-4 border border-gray-200 flex flex-col">
|
||||
<textarea
|
||||
v-model="message"
|
||||
placeholder="给小维AI 机器人发送消息"
|
||||
class="bg-transparent border-none outline-none w-full text-sm resize-none min-h-6"
|
||||
rows="2"
|
||||
@input="autoResize"
|
||||
ref="textareaRef"
|
||||
@keydown.enter.prevent="sendMessage"
|
||||
>
|
||||
</textarea>
|
||||
|
||||
<!-- 发送按钮 -->
|
||||
<div class="flex justify-end">
|
||||
<div class="flex justify-end mt-3">
|
||||
<button
|
||||
@click="sendMessage"
|
||||
:disabled="!message.trim()"
|
||||
class="flex items-center justify-center bg-[#4d6bfe] rounded-full w-8 h-8 border border-[#4d6bfe] hover:bg-[#3b5bef] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<SvgIcon name="up-arrow" customCss="w-5 h-5 text-white"></SvgIcon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 提示文字 -->
|
||||
<div class="flex items-center justify-center text-xs text-gray-400 mt-2">
|
||||
内容由 AI 生成,请仔细甄别
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, ref, nextTick } from 'vue'
|
||||
import StreamMarkdownRender from '@/components/StreamMarkdownRender.vue'
|
||||
|
||||
interface ChatMessage {
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
}
|
||||
|
||||
// 输入的消息
|
||||
const message = ref<string>('')
|
||||
|
||||
// 聊天容器引用
|
||||
const chatContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
// textarea引用
|
||||
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||||
|
||||
// 当前 SSE 连接实例
|
||||
let eventSource: EventSource | null = null
|
||||
|
||||
// 聊天记录 (给个默认的问候语)
|
||||
const chatList = ref<ChatMessage[]>([
|
||||
{
|
||||
role: 'assistant',
|
||||
content:
|
||||
'我是小维智能 AI 助手!✨ 我可以帮你解答各种问题,无论是学习、工作,还是日常生活中的小困惑,都可以找我聊聊。有什么我可以帮你的吗?😊',
|
||||
},
|
||||
])
|
||||
|
||||
// 发送消息
|
||||
const sendMessage = async (): Promise<void> => {
|
||||
// 校验发送的消息不能为空
|
||||
if (!message.value.trim()) return
|
||||
|
||||
// 将用户发送的消息添加到 chatList 聊天列表中
|
||||
const userMessage = message.value.trim()
|
||||
chatList.value.push({ role: 'user', content: userMessage })
|
||||
|
||||
// 点击发送按钮后,清空输入框
|
||||
message.value = ''
|
||||
// 将输入框的高度重置
|
||||
if (textareaRef.value) {
|
||||
textareaRef.value.style.height = 'auto'
|
||||
}
|
||||
|
||||
// 发送新的消息前关闭旧的 SSE 连接
|
||||
closeSSE()
|
||||
|
||||
// 添加一个占位的回复消息
|
||||
chatList.value.push({ role: 'assistant', content: '' })
|
||||
|
||||
const updateAssistantMessage = (content: string) => {
|
||||
const lastIndex = chatList.value.length - 1
|
||||
if (lastIndex < 0) return
|
||||
const lastMessage = chatList.value[lastIndex]
|
||||
if (lastMessage?.role !== 'assistant') return
|
||||
lastMessage.content = content
|
||||
}
|
||||
|
||||
try {
|
||||
// 建立 SSE 连接
|
||||
eventSource = new EventSource(
|
||||
`http://localhost:8080/v6/ai/generateStream?message=${encodeURIComponent(userMessage)}`,
|
||||
)
|
||||
|
||||
// 响应的回答
|
||||
let responseText = ''
|
||||
|
||||
// 处理消息事件
|
||||
eventSource.onmessage = (event) => {
|
||||
console.log('接收到数据: ', event.data)
|
||||
if (event.data) {
|
||||
// 若响应数据不为空
|
||||
// 持续追加流式回答
|
||||
const response = JSON.parse(event.data)
|
||||
responseText += response.v
|
||||
|
||||
// 更新最后一条消息
|
||||
updateAssistantMessage(responseText)
|
||||
// 滚动到底部
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理错误
|
||||
eventSource.onerror = (error) => {
|
||||
// 通常 SSE 在完成传输后会触发一次 error 事件,这是正常的
|
||||
if (error.eventPhase === EventSource.CLOSED) {
|
||||
console.log('SSE正常关闭')
|
||||
} else {
|
||||
// 提示用户 “请求出错”
|
||||
updateAssistantMessage('抱歉,请求出错了,请稍后重试。')
|
||||
// 滚动到底部
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
// 关闭 SSE
|
||||
closeSSE()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送消息错误: ', error)
|
||||
// 提示用户 “请求出错”
|
||||
updateAssistantMessage('抱歉,请求出错了,请稍后重试。')
|
||||
closeSSE()
|
||||
// 滚动到底部
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭 SSE 连接
|
||||
const closeSSE = () => {
|
||||
if (eventSource) {
|
||||
eventSource.close()
|
||||
eventSource = null
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = async () => {
|
||||
await nextTick() // 等待 Vue.js 完成 DOM 更新
|
||||
if (chatContainer.value) {
|
||||
// 若容器存在
|
||||
// 将容器的滚动条位置设置到最底部
|
||||
const container = chatContainer.value
|
||||
container.scrollTop = container.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
// 组件卸载时自动关闭连接
|
||||
onBeforeUnmount(() => {
|
||||
closeSSE()
|
||||
})
|
||||
|
||||
// 自动调整文本域高度
|
||||
const autoResize = () => {
|
||||
const textarea = textareaRef.value
|
||||
if (textarea) {
|
||||
// 重置高度以获取正确的滚动高度
|
||||
textarea.style.height = 'auto'
|
||||
|
||||
// 计算新高度,但最大不超过 300px
|
||||
const newHeight = Math.min(textarea.scrollHeight, 300)
|
||||
textarea.style.height = newHeight + 'px'
|
||||
|
||||
// 如果内容超出 300px,则启用滚动
|
||||
textarea.style.overflowY = textarea.scrollHeight > 300 ? 'auto' : 'hidden'
|
||||
}
|
||||
}
|
||||
import Layout from '@/layouts/BasicLayout.vue'
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.quesiton-container {
|
||||
font-size: 16px;
|
||||
line-height: 28px;
|
||||
color: #262626;
|
||||
padding: calc((44px - 28px) / 2) 20px;
|
||||
box-sizing: border-box;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background-color: #eff6ff;
|
||||
border-radius: 14px;
|
||||
max-width: calc(100% - 48px);
|
||||
position: relative;
|
||||
}
|
||||
/* 聊天内容区域样式 */
|
||||
.overflow-y-auto {
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent; /* 自定义滚动条颜色 */
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user