|
@@ -131,9 +131,20 @@
|
|
|
<template v-if="isImageFile(file)">
|
|
|
<div class="image-preview">
|
|
|
<img :src="file.url" class="preview-image" />
|
|
|
- <!-- <div class="image-info">
|
|
|
- <span>解析图片</span>
|
|
|
- </div> -->
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 视频文件显示 -->
|
|
|
+ <template v-else-if="isVideoFile(file)">
|
|
|
+ <div class="video-preview">
|
|
|
+ <video
|
|
|
+ controls
|
|
|
+ class="preview-video"
|
|
|
+ :src="file.url"
|
|
|
+ preload="metadata"
|
|
|
+ >
|
|
|
+ Your browser does not support the video tag.
|
|
|
+ </video>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
@@ -142,11 +153,10 @@
|
|
|
<div class="file-info-display">
|
|
|
<img :src="getFileIcon(file)" class="file-type-icon" />
|
|
|
<div class="file-details">
|
|
|
- <span class="file-name">{{ file.name }}</span>
|
|
|
+ <span class="file-name" :title="file.name">{{ file.name }}</span>
|
|
|
<span class="file-size">{{ file.size }}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <!-- <div class="file-action">解析文档中的内容</div> -->
|
|
|
</template>
|
|
|
</div>
|
|
|
</template>
|
|
@@ -223,15 +233,24 @@
|
|
|
@dragover.prevent
|
|
|
@dragenter.prevent
|
|
|
>
|
|
|
- <img
|
|
|
- src="../assets/svg/upload.svg"
|
|
|
- alt="upload"
|
|
|
- class="upload-icon"
|
|
|
- />
|
|
|
- <div class="upload-text">Upload files</div>
|
|
|
- <div class="upload-hint">
|
|
|
- Click or drag files to this area to upload
|
|
|
- </div>
|
|
|
+ <!-- 移除条件判断,让整个区域始终可点击 -->
|
|
|
+ <template v-if="isUploading">
|
|
|
+ <div class="upload-loading">
|
|
|
+ <div class="loading-spinner"></div>
|
|
|
+ <div class="upload-text">Uploading...</div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <img
|
|
|
+ src="../assets/svg/upload.svg"
|
|
|
+ alt="upload"
|
|
|
+ class="upload-icon"
|
|
|
+ />
|
|
|
+ <div class="upload-text">Upload files</div>
|
|
|
+ <div class="upload-hint">
|
|
|
+ Click or drag files to this area to upload
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
<input
|
|
|
type="file"
|
|
|
ref="fileInput"
|
|
@@ -251,12 +270,36 @@
|
|
|
:class="{ 'is-image': isImageFile(file) }"
|
|
|
>
|
|
|
<div :class="isImageFile(file) ? 'file-wrapper' : ''">
|
|
|
+ <!-- 添加文件 loading 状态 -->
|
|
|
+ <div v-if="file.isUploading" class="file-loading">
|
|
|
+ <div class="loading-spinner"></div>
|
|
|
+ </div>
|
|
|
+ <!-- 其他文件显示逻辑保持不变 -->
|
|
|
+ <img
|
|
|
+ v-else-if="isImageFile(file)"
|
|
|
+ :src="file.url"
|
|
|
+ alt="file"
|
|
|
+ class="file-icon"
|
|
|
+ />
|
|
|
+ <video
|
|
|
+ v-else-if="isVideoFile(file)"
|
|
|
+ :src="file.url"
|
|
|
+ class="file-icon"
|
|
|
+ preload="metadata"
|
|
|
+ >
|
|
|
+ Your browser does not support the video tag.
|
|
|
+ </video>
|
|
|
<img
|
|
|
+ v-else
|
|
|
:src="getFileIcon(file)"
|
|
|
alt="file"
|
|
|
- :class="isImageFile(file) ? 'file-icon' : ''"
|
|
|
+ :class="isImageFile(file) || isVideoFile(file) ? 'file-icon' : ''"
|
|
|
/>
|
|
|
- <button class="remove-btn" @click.stop="removeFile(file)">
|
|
|
+ <button
|
|
|
+ class="remove-btn"
|
|
|
+ @click.stop="removeFile(file)"
|
|
|
+ v-if="!file.isUploading"
|
|
|
+ >
|
|
|
×
|
|
|
</button>
|
|
|
</div>
|
|
@@ -411,6 +454,9 @@ const isPlaying = ref(false);
|
|
|
// 添加当前播放消息的 ID
|
|
|
const currentPlayingId = ref(null);
|
|
|
|
|
|
+// 添加新的响应式变量
|
|
|
+const isUploading = ref(false);
|
|
|
+
|
|
|
// 方法
|
|
|
const toggleAttachments = () => {
|
|
|
showAttachments.value = !showAttachments.value;
|
|
@@ -421,7 +467,9 @@ const removeFile = (file) => {
|
|
|
};
|
|
|
|
|
|
const triggerFileInput = () => {
|
|
|
- fileInput.value.click();
|
|
|
+ // 如果正在上传,不触发新的上传
|
|
|
+ if (isUploading.value) return;
|
|
|
+ fileInput.value?.click();
|
|
|
};
|
|
|
|
|
|
const uploadFile = async (file) => {
|
|
@@ -452,28 +500,61 @@ const uploadFile = async (file) => {
|
|
|
|
|
|
const handleFileUpload = async (event) => {
|
|
|
const files = Array.from(event.target.files);
|
|
|
+ if (files.length === 0) return;
|
|
|
+ // 添加文件大小检查
|
|
|
+ const MAX_VIDEO_SIZE = 10 * 1024 * 1024; // 10MB in bytes
|
|
|
+ const invalidFiles = files.filter(file => {
|
|
|
+ if (isVideoFile(file) && file.size > MAX_VIDEO_SIZE) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ });
|
|
|
|
|
|
- try {
|
|
|
- isLoading.value = true;
|
|
|
+ if (invalidFiles.length > 0) {
|
|
|
+ // 如果有超出大小限制的视频文件,显示错误提示
|
|
|
+ alert('Video files must not exceed 10MB');
|
|
|
+ event.target.value = ''; // 清空input
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ isUploading.value = true;
|
|
|
|
|
|
- const uploadPromises = files.map(async (file) => {
|
|
|
- const url = await uploadFile(file);
|
|
|
- return {
|
|
|
+ try {
|
|
|
+ for (const file of files) {
|
|
|
+ // 创建一个带有上传状态的文件对象
|
|
|
+ const fileWithStatus = {
|
|
|
name: file.name,
|
|
|
- size: file.size, // 保存原始大小(字节)
|
|
|
- sizeFormatted: formatFileSize(file.size), // 格式化后的大小
|
|
|
+ size: file.size,
|
|
|
+ sizeFormatted: formatFileSize(file.size),
|
|
|
type: file.type,
|
|
|
- url: url,
|
|
|
+ isUploading: true
|
|
|
};
|
|
|
- });
|
|
|
-
|
|
|
- const uploadedFiles = await Promise.all(uploadPromises);
|
|
|
- attachedFiles.value = [...attachedFiles.value, ...uploadedFiles];
|
|
|
+
|
|
|
+ // 先添加到文件列表中
|
|
|
+ attachedFiles.value.push(fileWithStatus);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const url = await uploadFile(file);
|
|
|
+ // 直接修改数组中的对象
|
|
|
+ const index = attachedFiles.value.findIndex(f => f.name === file.name);
|
|
|
+ if (index !== -1) {
|
|
|
+ // 使用数组替换方式更新,确保响应式更新
|
|
|
+ attachedFiles.value.splice(index, 1, {
|
|
|
+ ...fileWithStatus,
|
|
|
+ url,
|
|
|
+ isUploading: false
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ // 如果上传失败,从列表中移除该文件
|
|
|
+ attachedFiles.value = attachedFiles.value.filter(f => f.name !== file.name);
|
|
|
+ console.error("单个文件上传失败:", error);
|
|
|
+ }
|
|
|
+ }
|
|
|
} catch (error) {
|
|
|
console.error("文件上传失败:", error);
|
|
|
} finally {
|
|
|
- isLoading.value = false;
|
|
|
- event.target.value = "";
|
|
|
+ isUploading.value = false;
|
|
|
+ event.target.value = ""; // 清空input以允许重复上传相同文件
|
|
|
}
|
|
|
};
|
|
|
|
|
@@ -501,10 +582,19 @@ const isImageFile = (file) => {
|
|
|
return imageTypes.includes(ext);
|
|
|
};
|
|
|
|
|
|
+const isVideoFile = (file) => {
|
|
|
+ const videoTypes = ["mp4", "webm", "ogg"];
|
|
|
+ const ext = file.name?.split(".")?.pop()?.toLowerCase() || "";
|
|
|
+ return videoTypes.includes(ext);
|
|
|
+};
|
|
|
+
|
|
|
const getFileType = (file) => {
|
|
|
const ext = file.name.split(".").pop().toLowerCase();
|
|
|
const imageTypes = ["jpg", "jpeg", "png", "gif", "webp"];
|
|
|
- return imageTypes.includes(ext);
|
|
|
+ const videoTypes = ["mp4", "webm", "ogg"];
|
|
|
+ if (imageTypes.includes(ext)) return "image";
|
|
|
+ if (videoTypes.includes(ext)) return "video";
|
|
|
+ return "document";
|
|
|
};
|
|
|
|
|
|
// 添加滚动到底部的方法
|
|
@@ -531,11 +621,15 @@ const sendMessage = async () => {
|
|
|
|
|
|
// 收集文件URLs
|
|
|
const imageUrls = [];
|
|
|
+ const videoUrls = [];
|
|
|
const documentUrls = [];
|
|
|
|
|
|
attachedFiles.value.forEach((file) => {
|
|
|
- if (getFileType(file)) {
|
|
|
+ const fileType = getFileType(file);
|
|
|
+ if (fileType === "image") {
|
|
|
imageUrls.push(file.url);
|
|
|
+ } else if (fileType === "video") {
|
|
|
+ videoUrls.push(file.url);
|
|
|
} else {
|
|
|
documentUrls.push(file.url);
|
|
|
}
|
|
@@ -580,11 +674,12 @@ const sendMessage = async () => {
|
|
|
`${import.meta.env.VITE_BASE_AI_API}/chatbot/multimodal`,
|
|
|
{
|
|
|
message: currentInput,
|
|
|
- chat_config_id: "17",
|
|
|
+ chat_config_id: "2",
|
|
|
user_id: "13365429324",
|
|
|
session_id: session_id.value,
|
|
|
source: "pc",
|
|
|
image_urls: imageUrls,
|
|
|
+ video_urls: videoUrls,
|
|
|
documents: documentUrls,
|
|
|
}
|
|
|
);
|
|
@@ -690,26 +785,54 @@ const toggleRightMenu = () => {
|
|
|
};
|
|
|
|
|
|
const handleFileDrop = async (event) => {
|
|
|
+ if (isUploading.value) return;
|
|
|
+
|
|
|
const files = Array.from(event.dataTransfer.files);
|
|
|
+ if (files.length === 0) return;
|
|
|
+// 添加文件大小检查
|
|
|
+const MAX_VIDEO_SIZE = 10 * 1024 * 1024; // 10MB in bytes
|
|
|
+ const invalidFiles = files.filter(file => {
|
|
|
+ if (isVideoFile(file) && file.size > MAX_VIDEO_SIZE) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ });
|
|
|
+
|
|
|
+ isUploading.value = true;
|
|
|
+
|
|
|
try {
|
|
|
- isLoading.value = true;
|
|
|
- const uploadPromises = files.map(async (file) => {
|
|
|
- const url = await uploadFile(file);
|
|
|
- return {
|
|
|
+ for (const file of files) {
|
|
|
+ const fileWithStatus = {
|
|
|
name: file.name,
|
|
|
- size: file.size, // 保存原始大小(字节)
|
|
|
- sizeFormatted: formatFileSize(file.size), // 格式化后的大小
|
|
|
+ size: file.size,
|
|
|
+ sizeFormatted: formatFileSize(file.size),
|
|
|
type: file.type,
|
|
|
- url: url,
|
|
|
+ isUploading: true
|
|
|
};
|
|
|
- });
|
|
|
-
|
|
|
- const uploadedFiles = await Promise.all(uploadPromises);
|
|
|
- attachedFiles.value = [...attachedFiles.value, ...uploadedFiles];
|
|
|
+
|
|
|
+ attachedFiles.value.push(fileWithStatus);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const url = await uploadFile(file);
|
|
|
+ // 直接修改数组中的对象
|
|
|
+ const index = attachedFiles.value.findIndex(f => f.name === file.name);
|
|
|
+ if (index !== -1) {
|
|
|
+ // 使用数组替换方式更新,确保响应式更新
|
|
|
+ attachedFiles.value.splice(index, 1, {
|
|
|
+ ...fileWithStatus,
|
|
|
+ url,
|
|
|
+ isUploading: false
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ attachedFiles.value = attachedFiles.value.filter(f => f.name !== file.name);
|
|
|
+ console.error("单个文件上传失败:", error);
|
|
|
+ }
|
|
|
+ }
|
|
|
} catch (error) {
|
|
|
console.error("文件上传失败:", error);
|
|
|
} finally {
|
|
|
- isLoading.value = false;
|
|
|
+ isUploading.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
@@ -1141,7 +1264,10 @@ const renderMarkdown = (content) => {
|
|
|
object-fit: cover;
|
|
|
border-radius: 4px;
|
|
|
}
|
|
|
-
|
|
|
+/* 视频预览特定样式 */
|
|
|
+.file-wrapper video.file-icon {
|
|
|
+ background: #000; /* 视频加载前的背景色 */
|
|
|
+}
|
|
|
.remove-btn {
|
|
|
position: absolute;
|
|
|
top: -8px;
|
|
@@ -1172,12 +1298,21 @@ const renderMarkdown = (content) => {
|
|
|
|
|
|
.file-name {
|
|
|
display: block;
|
|
|
+ max-width: 150px; /* 设置最大宽度 */
|
|
|
color: rgba(0, 0, 0, 0.88);
|
|
|
font-size: 14px;
|
|
|
white-space: nowrap;
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
}
|
|
|
+.file-info .file-name {
|
|
|
+ max-width: 150px; /* 可以根据实际布局调整宽度 */
|
|
|
+}
|
|
|
+
|
|
|
+/* 如果需要在hover时显示完整文件名,可以添加 title 属性和 tooltip 效果 */
|
|
|
+.file-name:hover {
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
|
|
|
.file-size {
|
|
|
display: block;
|
|
@@ -1550,11 +1685,11 @@ const renderMarkdown = (content) => {
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
-.file-name {
|
|
|
+/* .file-name {
|
|
|
font-size: 14px;
|
|
|
color: rgba(0, 0, 0, 0.88);
|
|
|
margin-bottom: 4px;
|
|
|
-}
|
|
|
+} */
|
|
|
|
|
|
.file-size {
|
|
|
font-size: 12px;
|
|
@@ -1625,11 +1760,11 @@ const renderMarkdown = (content) => {
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
-.file-name {
|
|
|
+/* .file-name {
|
|
|
font-size: 14px;
|
|
|
color: rgba(0, 0, 0, 0.88);
|
|
|
margin-bottom: 4px;
|
|
|
-}
|
|
|
+} */
|
|
|
|
|
|
.file-size {
|
|
|
font-size: 12px;
|
|
@@ -1766,6 +1901,67 @@ const renderMarkdown = (content) => {
|
|
|
font-size: 14px;
|
|
|
color: #1296db;
|
|
|
}
|
|
|
+.video-preview {
|
|
|
+ position: relative;
|
|
|
+ width: 300px;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #fff;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-video {
|
|
|
+ width: 100%;
|
|
|
+ height: auto;
|
|
|
+ display: block;
|
|
|
+ max-height: 300px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 添加 loading 相关样式 */
|
|
|
+.upload-loading {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-spinner {
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ border: 2px solid #f3f3f3;
|
|
|
+ border-top: 2px solid #1677ff;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.file-loading {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(255, 255, 255, 0.8);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ 0% { transform: rotate(0deg); }
|
|
|
+ 100% { transform: rotate(360deg); }
|
|
|
+}
|
|
|
+
|
|
|
+/* 上传区域在上传时的样式 */
|
|
|
+.upload-area {
|
|
|
+ position: relative;
|
|
|
+ cursor: pointer; /* 始终保持指针样式 */
|
|
|
+}
|
|
|
+
|
|
|
+.upload-area.uploading {
|
|
|
+ opacity: 0.7;
|
|
|
+}
|
|
|
</style>
|
|
|
<style lang="less" scoped>
|
|
|
::v-deep .message-content {
|