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>
|