mirror of
https://gitee.com/ccnetcore/Yi
synced 2026-04-29 04:33:24 +08:00
336 lines
7.3 KiB
Vue
336 lines
7.3 KiB
Vue
<!-- 默认消息列表页 -->
|
||
<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>
|