Files
Yi.Framework/Yi.Ai.Vue3/src/pages/chat/components/ChatSender.vue
2026-01-31 23:38:39 +08:00

207 lines
4.4 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 { ElIcon } from 'element-plus';
import { watch, nextTick, ref } from 'vue';
import { Sender } from 'vue-element-plus-x';
import ModelSelect from '@/components/ModelSelect/index.vue';
import { useFilesStore } from '@/stores/modules/files';
const props = defineProps<{
/** 是否加载中 */
loading?: boolean;
/** 是否显示发送按钮 */
showSend?: boolean;
/** 最小行数 */
minRows?: number;
/** 最大行数 */
maxRows?: number;
/** 是否只读模式 */
readOnly?: boolean;
}>();
const emit = defineEmits<{
(e: 'submit', value: string): void;
(e: 'cancel'): void;
}>();
const modelValue = defineModel<string>({ default: '' });
const filesStore = useFilesStore();
const senderRef = ref<InstanceType<typeof Sender> | null>(null);
/**
* 删除文件卡片
*/
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();
}
});
},
);
defineExpose({
senderRef,
focus: () => senderRef.value?.focus(),
blur: () => senderRef.value?.blur(),
});
</script>
<template>
<Sender
ref="senderRef"
v-model="modelValue"
class="chat-sender"
:auto-size="{
maxRows: maxRows ?? 6,
minRows: minRows ?? 2,
}"
variant="updown"
clearable
allow-speech
:loading="loading"
:read-only="readOnly"
@submit="(v) => emit('submit', v)"
@cancel="emit('cancel')"
>
<!-- 头部文件附件区域 -->
<template #header>
<div class="chat-sender__header">
<Attachments
:items="filesStore.filesList"
:hide-upload="true"
@delete-card="handleDeleteCard"
>
<!-- 左侧滚动按钮 -->
<template #prev-button="{ show, onScrollLeft }">
<div
v-if="show"
class="chat-sender__scroll-btn chat-sender__scroll-btn--prev"
@click="onScrollLeft"
>
<ElIcon><ArrowLeftBold /></ElIcon>
</div>
</template>
<!-- 右侧滚动按钮 -->
<template #next-button="{ show, onScrollRight }">
<div
v-if="show"
class="chat-sender__scroll-btn chat-sender__scroll-btn--next"
@click="onScrollRight"
>
<ElIcon><ArrowRightBold /></ElIcon>
</div>
</template>
</Attachments>
</div>
</template>
<!-- 前缀文件选择和模型选择 -->
<template #prefix>
<div class="chat-sender__prefix">
<FilesSelect />
<ModelSelect />
</div>
</template>
<!-- 后缀加载动画 -->
<template #suffix>
<ElIcon v-if="loading" class="chat-sender__loading">
<Loading />
</ElIcon>
</template>
</Sender>
</template>
<style scoped lang="scss">
.chat-sender {
width: 100%;
&__header {
padding: 12px 12px 0 12px;
}
&__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 {
left: 8px;
}
&--next {
right: 8px;
}
}
&__loading {
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-sender {
&__prefix {
flex-wrap: wrap;
gap: 6px;
}
}
}
</style>