mirror of
https://gitee.com/ccnetcore/Yi
synced 2026-04-29 04:33:24 +08:00
207 lines
4.4 KiB
Vue
207 lines
4.4 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 { 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>
|