文本 Arduino C Python C++ Java C# Bash Go kotlin JavaScript HTML/XML CSS SQL <script setup lang="ts">
import {computed, nextTick, onMounted, reactive, ref, watch} from "vue";
import type {Conversation} from "@/types/conversation.ts";
import {findConversationList, insertConversation, removeConversationById} from "@/api/conversation.ts";
import {type ChatHistoryVO, ChatRoleEnum} from "@/types/chat.ts";
import {findChatHistory, sendMessage} from "@/api/chat.ts";
import {useRoute, useRouter} from "vue-router";
const router = useRouter()
const route = useRoute()
const textareaRef = ref<HTMLTextAreaElement | null>(null)
const handleKeyDown = (e: any) => {
// 过滤中文输入法状态下的 Enter(例如选词敲回车不触发发送或换行)
if (e.isComposing) return
// 判断是否按下了组合键 shift+enter
const hasModifierKey = e.shiftKey
if (hasModifierKey) {
// 组合键按下:阻止默认行为并插入换行
e.preventDefault()
const el = textareaRef.value
if (!el) return
const start = el.selectionStart
const end = el.selectionEnd
// // 在光标处插入换行
content.value = content.value.substring(0, start) + '\n' + content.value.substring(end)
// 保持光标位置在新行的开头
nextTick(() => {
el.selectionStart = el.selectionEnd = start + 1
})
} else {
// 单独按下 Enter:阻止默认换行,触发发送
e.preventDefault()
sendHandle()
}
}
// 最大高度限制 (px)
const maxHeight = window.innerHeight * 0.5
// 动态调整高度
const adjustHeight = () => {
const el = textareaRef.value
if (!el) return
// 先重置高度
el.style.height = 'auto'
// 根据滚动高度动态计算
if (el.scrollHeight > maxHeight) {
el.style.height = `${maxHeight}px`
el.style.overflowY = 'auto'
} else {
el.style.height = `${el.scrollHeight}px`
el.style.overflowY = 'hidden'
}
}
// 输入的内容
const content = ref<string>("");
watch(content, () => {
nextTick(() => {
adjustHeight()
})
})
/**
* 是否存在聊天
*/
const isExistsChat = computed(() => {
return !!conversationId.value &&
chatHistory.value.length > 0
})
// 会话id
const conversationId = ref<string>("");
/**
* 删除会话
*/
const removeConversation = () => {
removeConversationById(conversationId.value).then(response => {
if (response.code == 200) {
router.push('/chat')
window.location.reload()
}
})
}
// 对话记录列表
const chatHistory = ref<ChatHistoryVO[]>([])
/**
* 查询聊天记录
*/
const findHistory = () => {
findChatHistory(conversationId.value).then(response => {
if (response.code == 200) {
chatHistory.value = response.data
}
})
}
onMounted(() => {
// 获取并赋值
conversationId.value = route.params.conversationId as string || ''
// 有对话id就查询聊天记录
if (!!conversationId.value) {
findHistory()
}
})
// 会话列表
const conversationList = ref<Conversation[]>([])
/**
* 加载会话列表
*/
const loadConversationList = () => {
findConversationList().then(response => {
if (response.code == 200) {
conversationList.value = response.data
}
})
}
loadConversationList()
/**
* 创建对话
*/
const createConversation = () => {
insertConversation(content.value).then(response => {
if (response.code == 200) {
conversationId.value = response.data
conversationList.value.unshift({id: conversationId.value, title: content.value})
// 刷新路由参数
router.push({
name: 'Chat',
params: {
conversationId: conversationId.value
}
})
chatMessage()
}
})
}
/**
* 发送按钮处理
*/
const sendHandle = async () => {
// 为空判断
if (!content.value.trim()) {
alert("输入的内容不能为空")
return;
}
// 如果没有会话id就创建对话先
if (!conversationId.value) {
createConversation()
} else {
chatMessage()
}
}
const chatContainerRef = ref<HTMLElement | null>(null)
// 监听自动跳转到底部
watch(() => chatHistory.value,
async () => {
// 等待 Vue 把新蹦出来的字渲染到 DOM 树中
await nextTick()
chatContainerRef.value!.scrollTo({top: chatContainerRef.value!.scrollHeight, behavior: "auto"});
},
{deep: true}
)
// 是否发送中
const sending = ref<boolean>(false)
/**
* 聊天消息
*/
const chatMessage = () => {
sending.value = true;
// 添加用户自己的消息
chatHistory.value.push({
role: ChatRoleEnum.USER,
content: content.value,
});
let history = reactive({role: ChatRoleEnum.ASSISTANT, content: ''});
// 添加ai消息
chatHistory.value.push(history);
// 创建传输实体
const dto = {id: conversationId.value, content: content.value}
// 情况输入框的值
content.value = "";
sendMessage(dto, (text: string) => {
history.content += text
}).finally(() => {
sending.value = false;
})
}
</script>
<template>
<div class="chat__container">
<aside class="chat__aside">
<div class="aside__header">
<img src="@/assets/logo.png" class="chat__logo" alt="logo"/>
<span>知屿AI</span>
</div>
<a href="/chat" class="aside__add-session">新建会话</a>
<!-- 会话列表 -->
<div class="aside__session-section">
<span class="aside__session-title">最近对话</span>
<div class="aside__session-list">
<a v-for="item in conversationList" :key="item.id"
:href="`/chat/${item.id}`"
:class="[ 'aside__session-item',
{ 'aside__session-item--active': conversationId === item.id }]">
<img src="@/assets/book.png" width="17" alt="img">
<span class="aside__session-text">{{ item.title }}</span>
</a>
</div>
</div>
</aside>
<!-- 主体内容部分 -->
<main class="chat__main">
<header class="chat__header">
<div class="chat__header__title">
<img src="@/assets/logo.png" class="chat__logo" alt="logo"/>
<span>AI 智能对话</span>
</div>
<img @click="removeConversation" src="@/assets/remove.png" class="chat__header__remove" alt="删除">
</header>
<div class="chat__main__wrapper">
<div class="chat__content" ref="chatContainerRef">
<div class="chat__content-list" v-if="isExistsChat">
<div :class="`chat__content-item chat__content--${item.role}`" v-for="(item,index) in chatHistory"
:key="index">
<div class="chat__content__avatar" v-if="item.role == ChatRoleEnum.ASSISTANT">
<img src="@/assets/logo.png" width="100%" alt="logo"/>
</div>
<div class="chat__content__text">
{{ item.content }}
</div>
</div>
</div>
<!-- 为空的样式 -->
<div class="chat__content-empty" v-else>
<img src="@/assets/logo.png" class="chat__content-empty__img" alt="logo"/>
<span class="chat__content-empty__title"> 你好,我是知屿 AI</span>
<span class="chat__content-empty__desc">清晰思考,自由创造。从一个问题开始。</span>
</div>
</div>
<div class="chat__input-container">
<textarea @keydown.enter="handleKeyDown" class="chat__input-wrapper"
v-model="content" ref="textareaRef" placeholder="有问题,尽管问"/>
<button class="chat-send-btn" :disabled="sending" @click="sendHandle">发送</button>
</div>
<div class="chat__main__footer">
内容由 AI 生成,请注意甄别与核实
</div>
</div>
</main>
</div>
</template>
<style lang="scss" scoped>
// 页面整体
.chat__container {
height: 100vh;
display: flex;
}
// 侧边栏部分
.chat__aside {
display: flex;
flex-direction: column;
gap: 25px;
padding: 20px 12px 12px;
background-color: #fcfcfc;
width: 240px;
border: 1px solid #eeeeee;
}
// 侧边栏标题
.aside__header {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
color: #333333;
font-weight: 600;
}
// logo
.chat__logo {
width: 30px;
}
// 新建会话按钮
.aside__add-session {
text-decoration: none;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
height: 40px;
background-color: #4e6ef2;
border-radius: 12px;
color: white;
font-size: 14px;
border: none;
cursor: pointer;
&:hover {
background-color: #405fdc;
}
}
// 侧边栏的会话记录部分
.aside__session-section {
min-height: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 5px;
}
// 侧边栏的会话记录小标题
.aside__session-title {
padding-left: 10px;
color: #94a3b8;
font-size: 12px;
font-weight: 500;
}
// 侧边栏会话列表
.aside__session-list {
overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
}
// 侧边栏会话项
.aside__session-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #64748b;
padding: 10px;
border-radius: 12px;
text-decoration: none;
flex-shrink: 0;
}
// 侧边栏选中样式
.aside__session-item--active{
color: #1e293b;
background-color: #f1f5f9;
}
// 侧边栏会话项文本
.aside__session-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
// 主体内容部分样式
.chat__main {
display: flex;
flex-direction: column;
flex: 1;
background-color: #f8f8f8;
}
// 头部
.chat__header {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30px;
height: 60px;
background-color: #fcfcfc;
border-bottom: 1px solid #eeeeee;
}
// 头部标题
.chat__header__title {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-size: 18px;
color: #333333;
font-weight: 600;
}
// 头部删除按钮
.chat__header__remove {
cursor: pointer;
width: 20px;
}
// 对话内容主部分
.chat__main__wrapper {
min-height: 0;
background-color: #f7f8fa;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
// 对话内容部分
.chat__content {
width: 100%;
padding: 26px 32px;
display: flex;
justify-content: center;
flex: 1;
overflow-y: auto;
// 滚动条轨道
&::-webkit-scrollbar {
width: 8px; // 滚动条宽度
}
// 滚动条滑块
&::-webkit-scrollbar-thumb {
background-color: #888; // 滑块颜色
border-radius: 4px; // 滑块圆角
}
// 滚动条滑块悬停状态
&::-webkit-scrollbar-thumb:hover {
background-color: #555; // 悬停时滑块颜色
}
// 滚动条轨道背景
&::-webkit-scrollbar-track {
background-color: #f1f1f1; // 轨道背景颜色
border-radius: 4px; // 轨道圆角
}
}
// 聊天内容列表
.chat__content-list {
width: 100%;
display: flex;
flex-direction: column;
gap: 26px;
}
// 聊天的每一项
.chat__content-item {
align-items: flex-start;
display: flex;
width: 100%;
gap: 12px;
min-width: 0;
}
// 聊天文本部分样式
.chat__content__text {
max-width: 80%;
font-size: 14px;
border-radius: 15px;
padding: 14px 18px;
line-height: 28px;
white-space: pre-wrap;
word-break: break-all;
overflow-wrap: anywhere;
}
// ai的内容样式
.chat__content--assistant {
justify-content: flex-start;
// 头像部分
.chat__content__avatar {
height: 40px;
width: 40px;
line-height: 80px;
}
// 文本部分
.chat__content__text {
color: #333333;
background-color: white;
// 为空显示正在思考
&:empty::after {
content: "正在思考...";
display: inline-block;
color: #999;
font-style: italic;
font-size: 14px;
}
}
}
// 用户的内容样式
.chat__content--user {
justify-content: flex-end;
// 文本部分
.chat__content__text {
color: white;
background-color: #4e6ef2;
}
}
// 对话空内容外层容器
.chat__content-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
// 对话空内容的图片
.chat__content-empty__img {
width: 55px;
margin-bottom: 20px;
}
// 空内容提示标题
.chat__content-empty__title {
font-size: 32px;
color: #1e293b;
font-weight: 600;
margin-bottom: 10px;
}
// 空内容提示描述
.chat__content-empty__desc {
font-size: 14px;
color: #64748b;
}
// 对话底部
.chat__main__footer {
padding: 12px;
text-align: center;
font-size: 12px;
color: #94a3b8;
}
// 输入部分的容器
.chat__input-container {
margin-top: 20px;
position: sticky;
display: flex;
align-items: flex-end;
gap: 10px;
padding: 8px 20px;
background-color: white;
border: 2px solid #eeeeee;
border-radius: 15px;
box-shadow: 0 8px 30px rgba(29, 39, 64, 0.08);
width: 90%;
max-width: 900px;
}
// 输入里部分
.chat__input-wrapper {
width: 100%;
flex: 1;
outline: none;
border: none;
resize: none;
box-sizing: border-box;
padding: 10px 0;
line-height: 1.5;
min-height: 50px;
font-size: 14px;
color: #333333;
}
// 发送按钮
.chat-send-btn {
height: 40px;
font-size: 12px;
padding: 8px 16px;
border-radius: 10px;
color: white;
background-color: #4e6ef2;
font-weight: 500;
border: none;
&:hover {
background-color: #405fdc;
}
}
// 禁用状态
.chat-send-btn:disabled {
background-color: #c0c4cc;
color: #ffffff;
cursor: not-allowed;
opacity: 0.7;
}
</style>