Files
Yi.Admin/Yi.Ai.Vue3/src/pages/chat/layouts/chatDefaul/index.vue
2026-01-02 22:47:09 +08:00

336 lines
7.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 默认消息列表页 -->
<script setup lang="ts">
import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard';
import { ArrowLeftBold, ArrowRightBold, Loading } from '@element-plus/icons-vue';
import { useDebounceFn } from '@vueuse/core';
import { ElMessage } from 'element-plus';
import { computed, nextTick, ref, watch } from 'vue';
import ModelSelect from '@/components/ModelSelect/index.vue';
import WelecomeText from '@/components/WelecomeText/index.vue';
import Collapse from '@/layouts/components0/Header/components/Collapse.vue';
import CreateChat from '@/layouts/components0/Header/components/CreateChat.vue';
import { useUserStore } from '@/stores';
import { useFilesStore } from '@/stores/modules/files';
import { useSessionStore } from '@/stores/modules/session';
// Store 实例
const userStore = useUserStore();
const sessionStore = useSessionStore();
const filesStore = useFilesStore();
// 计算属性
const currentSession = computed(() => sessionStore.currentSession);
// 响应式数据
const senderValue = ref(''); // 输入框内容
const senderRef = ref(); // Sender 组件引用
const isSending = ref(false); // 发送状态标志
/**
* 防抖发送消息函数
*/
const debouncedSend = useDebounceFn(
async () => {
// 1. 验证输入
if (!senderValue.value.trim()) {
ElMessage.warning('消息内容不能为空');
return;
}
// 2. 检查是否正在发送
if (isSending.value) {
ElMessage.warning('请等待上一条消息发送完成');
return;
}
// 3. 准备发送数据
const content = senderValue.value.trim();
isSending.value = true;
try {
// 4. 保存到本地存储(可选,用于页面刷新后恢复)
localStorage.setItem('chatContent', content);
// 5. 创建会话
await sessionStore.createSessionList({
userId: userStore.userInfo?.userId as number,
sessionContent: content,
sessionTitle: content.slice(0, 10),
remark: content.slice(0, 10),
});
// 6. 清空输入框
senderValue.value = '';
}
catch (error: any) {
console.error('发送消息失败:', error);
ElMessage.error(error.message || '发送消息失败');
}
finally {
// 7. 重置发送状态
isSending.value = false;
}
},
800, // 防抖延迟
{ leading: true, trailing: false }, // 立即执行第一次,忽略后续快速点击
);
/**
* 触发发送消息
*/
function handleSend() {
debouncedSend();
}
/**
* 删除文件卡片
* @param _item 文件项
* @param index 文件索引
*/
function handleDeleteCard(_item: FilesCardProps, index: number) {
filesStore.deleteFileByIndex(index);
}
/**
* 监听文件列表变化,自动展开/收起 Sender 头部
*/
watch(
() => filesStore.filesList.length,
(val) => {
nextTick(() => {
if (val > 0) {
senderRef.value?.openHeader();
}
else {
senderRef.value?.closeHeader();
}
});
},
);
</script>
<template>
<div class="chat-default">
<!-- 头部导航栏 -->
<div class="chat-header">
<div class="header-content">
<div class="header-left">
<Collapse />
<CreateChat />
</div>
</div>
</div>
<div class="chat-default-wrap">
<!-- 欢迎文本 -->
<WelecomeText />
<!-- 消息发送器 -->
<Sender
ref="senderRef"
v-model="senderValue"
class="chat-default-sender"
data-tour="chat-sender"
:auto-size="{
maxRows: 9,
minRows: 3,
}"
variant="updown"
clearable
allow-speech
:loading="isSending"
@submit="handleSend"
>
<!-- 头部文件附件区域 -->
<template #header>
<div class="sender-header">
<Attachments
:items="filesStore.filesList"
:hide-upload="true"
@delete-card="handleDeleteCard"
>
<!-- 左侧滚动按钮 -->
<template #prev-button="{ show, onScrollLeft }">
<div
v-if="show"
class="scroll-btn prev-btn"
@click="onScrollLeft"
>
<el-icon>
<ArrowLeftBold />
</el-icon>
</div>
</template>
<!-- 右侧滚动按钮 -->
<template #next-button="{ show, onScrollRight }">
<div
v-if="show"
class="scroll-btn next-btn"
@click="onScrollRight"
>
<el-icon>
<ArrowRightBold />
</el-icon>
</div>
</template>
</Attachments>
</div>
</template>
<!-- 前缀文件选择和模型选择 -->
<template #prefix>
<div class="sender-prefix">
<FilesSelect />
<ModelSelect />
</div>
</template>
<!-- 后缀发送加载动画 -->
<template #suffix>
<el-icon v-if="isSending" class="loading-icon">
<Loading />
</el-icon>
</template>
</Sender>
</div>
</div>
</template>
<style scoped lang="scss">
.chat-default {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 20px;
.chat-header {
width: 100%;
//max-width: 1000px;
height: 60px;
display: flex;
align-items: center;
flex-shrink: 0;
.header-content {
width: 100%;
display: flex;
align-items: center;
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
}
}
}
.chat-default-wrap {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
max-width: 800px;
min-height: 450px;
padding: 20px;
box-sizing: border-box;
.chat-default-sender {
width: 100%;
margin-top: 30px;
}
}
.sender-header {
padding: 12px 12px 0 12px;
}
.sender-prefix {
display: flex;
flex: 1;
align-items: center;
gap: 8px;
flex: none;
width: fit-content;
overflow: hidden;
}
.scroll-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.08);
color: rgba(0, 0, 0, 0.4);
background-color: #fff;
font-size: 10px;
cursor: pointer;
z-index: 10;
transition: all 0.2s ease;
&:hover {
background-color: #f3f4f6;
border-color: rgba(0, 0, 0, 0.15);
color: rgba(0, 0, 0, 0.6);
}
&.prev-btn {
left: 8px;
}
&.next-btn {
right: 8px;
}
}
.loading-icon {
margin-left: 8px;
color: var(--el-color-primary);
animation: rotating 2s linear infinite;
}
@keyframes rotating {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
// 响应式设计
@media (max-width: 768px) {
.chat-default {
padding: 0 12px;
.chat-header {
height: 50px;
}
}
.chat-default-wrap {
padding: 12px;
min-height: calc(100vh - 120px);
.chat-default-sender {
margin-top: 20px;
}
}
.sender-prefix {
flex-wrap: wrap;
gap: 6px;
}
}
</style>