feat: 消息ui优化

This commit is contained in:
Gsh
2026-01-31 23:38:39 +08:00
parent 6af3fb44f4
commit 3b6887dc2e
18 changed files with 2640 additions and 1938 deletions

View File

@@ -0,0 +1,206 @@
<!-- 聊天发送区域组件 -->
<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>