fix: 网页版增加对话图片支持
This commit is contained in:
@@ -7,7 +7,6 @@ import { ElMessage } from 'element-plus';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import ModelSelect from '@/components/ModelSelect/index.vue';
|
||||
import WelecomeText from '@/components/WelecomeText/index.vue';
|
||||
import { useGuideTour } from '@/hooks/useGuideTour';
|
||||
import { useGuideTourStore, useUserStore } from '@/stores';
|
||||
import { useFilesStore } from '@/stores/modules/files';
|
||||
|
||||
@@ -135,6 +134,8 @@ watch(
|
||||
</template>
|
||||
<template #prefix>
|
||||
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden">
|
||||
<FilesSelect />
|
||||
|
||||
<ModelSelect />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -13,7 +13,6 @@ import { Sender } from 'vue-element-plus-x';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { send } from '@/api';
|
||||
import ModelSelect from '@/components/ModelSelect/index.vue';
|
||||
import { useGuideTour } from '@/hooks/useGuideTour';
|
||||
import { useGuideTourStore } from '@/stores';
|
||||
import { useChatStore } from '@/stores/modules/chat';
|
||||
import { useFilesStore } from '@/stores/modules/files';
|
||||
@@ -30,6 +29,7 @@ type MessageItem = BubbleProps & {
|
||||
thinkingStatus?: ThinkingStatus;
|
||||
thinlCollapse?: boolean;
|
||||
reasoning_content?: string;
|
||||
images?: Array<{ url: string; name?: string }>; // 用户消息中的图片列表
|
||||
};
|
||||
|
||||
const route = useRoute();
|
||||
@@ -179,25 +179,85 @@ async function startSSE(chatContent: string) {
|
||||
if (isSending.value)
|
||||
return;
|
||||
|
||||
// 检查是否有未上传完成的文件
|
||||
const hasUnuploadedFiles = filesStore.filesList.some(f => !f.isUploaded);
|
||||
if (hasUnuploadedFiles) {
|
||||
ElMessage.warning('文件正在上传中,请稍候...');
|
||||
return;
|
||||
}
|
||||
|
||||
isSending.value = true;
|
||||
|
||||
try {
|
||||
// 清空输入框
|
||||
inputValue.value = '';
|
||||
addMessage(chatContent, true);
|
||||
|
||||
// 获取当前上传的图片(在清空之前保存)
|
||||
const imageFiles = filesStore.filesList.filter(f => f.isUploaded && f.file.type.startsWith('image/'));
|
||||
const images = imageFiles.map(f => ({
|
||||
url: f.base64!, // 使用base64作为URL
|
||||
name: f.name,
|
||||
}));
|
||||
|
||||
addMessage(chatContent, true, images);
|
||||
addMessage('', false);
|
||||
|
||||
// 立即清空文件列表(不要等到响应完成)
|
||||
filesStore.clearFilesList();
|
||||
|
||||
// 这里有必要调用一下 BubbleList 组件的滚动到底部 手动触发 自动滚动
|
||||
bubbleListRef.value?.scrollToBottom();
|
||||
|
||||
// 组装消息内容,支持图片
|
||||
const messagesContent = bubbleItems.value.slice(0, -1).slice(-6).map((item: MessageItem) => {
|
||||
const baseMessage: any = {
|
||||
role: item.role,
|
||||
};
|
||||
|
||||
// 如果是用户消息且有附件(图片),组装成数组格式
|
||||
if (item.role === 'user' && item.key === bubbleItems.value.length - 2) {
|
||||
// 当前发送的消息
|
||||
const contentArray: any[] = [];
|
||||
|
||||
// 添加文本内容
|
||||
if (item.content) {
|
||||
contentArray.push({
|
||||
type: 'text',
|
||||
text: item.content,
|
||||
});
|
||||
}
|
||||
|
||||
// 添加图片内容(使用之前保存的 imageFiles)
|
||||
imageFiles.forEach((fileItem) => {
|
||||
if (fileItem.base64) {
|
||||
contentArray.push({
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: fileItem.base64, // 使用base64
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 如果有图片或文件,使用数组格式
|
||||
if (contentArray.length > 1 || imageFiles.length > 0) {
|
||||
baseMessage.content = contentArray;
|
||||
} else {
|
||||
baseMessage.content = item.content;
|
||||
}
|
||||
} else {
|
||||
// 其他消息保持原样
|
||||
baseMessage.content = (item.role === 'ai' || item.role === 'assistant') && item.content.length > 10000
|
||||
? `${item.content.substring(0, 10000)}...(内容过长,已省略)`
|
||||
: item.content;
|
||||
}
|
||||
|
||||
return baseMessage;
|
||||
});
|
||||
|
||||
// 使用 for-await 处理流式响应
|
||||
for await (const chunk of stream({
|
||||
messages: bubbleItems.value.slice(0, -1).slice(-6).map((item: MessageItem) => ({
|
||||
role: item.role,
|
||||
content: (item.role === 'ai' || item.role === 'assistant') && item.content.length > 10000
|
||||
? `${item.content.substring(0, 10000)}...(内容过长,已省略)`
|
||||
: item.content,
|
||||
})),
|
||||
messages: messagesContent,
|
||||
sessionId: route.params?.id !== 'not_login' ? String(route.params?.id) : 'not_login',
|
||||
stream: true,
|
||||
userId: userStore.userInfo?.userId,
|
||||
@@ -250,7 +310,7 @@ async function cancelSSE() {
|
||||
}
|
||||
|
||||
// 添加消息 - 维护聊天记录
|
||||
function addMessage(message: string, isUser: boolean) {
|
||||
function addMessage(message: string, isUser: boolean, images?: Array<{ url: string; name?: string }>) {
|
||||
const i = bubbleItems.value.length;
|
||||
const obj: MessageItem = {
|
||||
key: i,
|
||||
@@ -267,6 +327,7 @@ function addMessage(message: string, isUser: boolean) {
|
||||
thinkingStatus: 'start',
|
||||
thinlCollapse: false,
|
||||
noStyle: !isUser,
|
||||
images: images && images.length > 0 ? images : undefined,
|
||||
};
|
||||
bubbleItems.value.push(obj);
|
||||
}
|
||||
@@ -301,6 +362,11 @@ function copy(item: any) {
|
||||
.then(() => ElMessage.success('已复制到剪贴板'))
|
||||
.catch(() => ElMessage.error('复制失败'));
|
||||
}
|
||||
|
||||
// 图片预览
|
||||
function handleImagePreview(url: string) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -317,9 +383,23 @@ function copy(item: any) {
|
||||
<template #content="{ item }">
|
||||
<!-- chat 内容走 markdown -->
|
||||
<XMarkdown v-if="item.content && (item.role === 'assistant' || item.role === 'system')" class="markdown-body" :markdown="item.content" :themes="{ light: 'github-light', dark: 'github-dark' }" default-theme-mode="dark" />
|
||||
<!-- user 内容 纯文本 -->
|
||||
<div v-if="item.content && item.role === 'user'" class="user-content">
|
||||
{{ item.content }}
|
||||
<!-- user 内容 纯文本 + 图片 -->
|
||||
<div v-if="item.role === 'user'" class="user-content-wrapper">
|
||||
<!-- 图片列表 -->
|
||||
<div v-if="item.images && item.images.length > 0" class="user-images">
|
||||
<img
|
||||
v-for="(image, index) in item.images"
|
||||
:key="index"
|
||||
:src="image.url"
|
||||
:alt="image.name || '图片'"
|
||||
class="user-image"
|
||||
@click="() => handleImagePreview(image.url)"
|
||||
>
|
||||
</div>
|
||||
<!-- 文本内容 -->
|
||||
<div v-if="item.content" class="user-content">
|
||||
{{ item.content }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -375,7 +455,7 @@ function copy(item: any) {
|
||||
</template>
|
||||
<template #prefix>
|
||||
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden">
|
||||
<!-- <FilesSelect /> -->
|
||||
<FilesSelect />
|
||||
<ModelSelect />
|
||||
</div>
|
||||
</template>
|
||||
@@ -421,6 +501,28 @@ function copy(item: any) {
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.user-content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.user-images {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.user-image {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
.user-content {
|
||||
// 换行
|
||||
white-space: pre-wrap;
|
||||
|
||||
Reference in New Issue
Block a user