feat(upload): 重构文件上传逻辑并添加全局上传任务管理

- 在dialogue store中添加globalUploadList和uploadTaskMap管理上传任务
- 修改PanelFooter.vue使用addUploadTask替代直接添加记录
- 在uploads.ts中完善上传失败处理和任务清理逻辑
- 在useTalkRecord.ts中加载完成后恢复上传任务
- 移除调试用的console.log语句
This commit is contained in:
Phoenix 2025-06-27 16:26:02 +08:00
parent efd61b30f4
commit 871e33990a
11 changed files with 7808 additions and 332 deletions

7350
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@ import {
Pic, Pic,
FolderUpload FolderUpload
} from '@icon-park/vue-next' } from '@icon-park/vue-next'
import {IosSend} from '@vicons/ionicons4' import { IosSend } from '@vicons/ionicons4'
import { bus } from '@/utils/event-bus' import { bus } from '@/utils/event-bus'
import { EditorConst } from '@/constant/event-bus' import { EditorConst } from '@/constant/event-bus'
import { emitCall } from '@/utils/common' import { emitCall } from '@/utils/common'
@ -33,7 +33,7 @@ const props = defineProps({
const emit = defineEmits(['editor-event']) const emit = defineEmits(['editor-event'])
const userStore = useUserStore() const userStore = useUserStore()
const dialogueStore = useDialogueStore() const dialogueStore = useDialogueStore()
console.log('dialogueStore',dialogueStore.talk.talk_type) console.log('dialogueStore', dialogueStore.talk.talk_type)
const editorDraftStore = useEditorDraftStore() const editorDraftStore = useEditorDraftStore()
const editorRef = ref(null) const editorRef = ref(null)
const content = ref('') const content = ref('')
@ -86,27 +86,27 @@ const handleInput = (event) => {
} }
const target = editorNode; const target = editorNode;
const editorClone = editorNode.cloneNode(true); const editorClone = editorNode.cloneNode(true);
editorClone.querySelectorAll('.editor-quote').forEach(quote => quote.remove()); editorClone.querySelectorAll('.editor-quote').forEach(quote => quote.remove());
let rawTextContent = editorClone.textContent || ''; let rawTextContent = editorClone.textContent || '';
const emojiImages = editorClone.querySelectorAll('img.editor-emoji'); const emojiImages = editorClone.querySelectorAll('img.editor-emoji');
if (emojiImages.length > 0) { if (emojiImages.length > 0) {
emojiImages.forEach(emoji => { emojiImages.forEach(emoji => {
const altText = emoji.getAttribute('alt'); const altText = emoji.getAttribute('alt');
if (altText) { if (altText) {
rawTextContent += altText; rawTextContent += altText;
} }
}); });
} }
editorContent.value = rawTextContent; editorContent.value = rawTextContent;
const currentText = editorNode.textContent.trim(); const currentText = editorNode.textContent.trim();
const hasSpecialElements = editorNode.querySelector('img, .editor-file, .mention'); const hasSpecialElements = editorNode.querySelector('img, .editor-file, .mention');
if (currentText === '' && !hasSpecialElements) { if (currentText === '' && !hasSpecialElements) {
if (editorNode.innerHTML !== '') { if (editorNode.innerHTML !== '') {
editorNode.innerHTML = ''; editorNode.innerHTML = '';
} }
} }
editorHtml.value = editorNode.innerHTML || ''; editorHtml.value = editorNode.innerHTML || '';
const currentEditorItems = parseEditorContent().items; const currentEditorItems = parseEditorContent().items;
checkMention(target); checkMention(target);
saveDraft(); saveDraft();
emit('editor-event', { emit('editor-event', {
@ -114,7 +114,7 @@ const handleInput = (event) => {
data: currentEditorItems.reduce((result, item) => { data: currentEditorItems.reduce((result, item) => {
if (item.type === 1) return result + item.content; if (item.type === 1) return result + item.content;
if (item.type === 3) return result + '[图片]'; if (item.type === 3) return result + '[图片]';
return result; return result;
}, '') }, '')
}); });
}; };
@ -138,7 +138,7 @@ const checkMention = (target) => {
} }
const showMentionList = () => { const showMentionList = () => {
const query = currentMentionQuery.value.toLowerCase() const query = currentMentionQuery.value.toLowerCase()
mentionList.value = [{ id: 0, nickname: '全体成员', avatar: defAvatar, value: '全体成员' },...props.members].filter(member => { mentionList.value = [{ id: 0, nickname: '全体成员', avatar: defAvatar, value: '全体成员' }, ...props.members].filter(member => {
return member.value.toLowerCase().startsWith(query) && member.id !== userStore.uid return member.value.toLowerCase().startsWith(query) && member.id !== userStore.uid
}) })
showMention.value = mentionList.value.length > 0 showMention.value = mentionList.value.length > 0
@ -435,9 +435,9 @@ const handleKeydown = (event) => {
event.preventDefault(); event.preventDefault();
const messageData = parseEditorContent(); const messageData = parseEditorContent();
const isEmptyMessage = messageData.items.length === 0 || const isEmptyMessage = messageData.items.length === 0 ||
(messageData.items.length === 1 && (messageData.items.length === 1 &&
messageData.items[0].type === 1 && messageData.items[0].type === 1 &&
!messageData.items[0].content.trimEnd()); !messageData.items[0].content.trimEnd());
if (isEmptyMessage) { if (isEmptyMessage) {
if (editor.innerHTML.trim() !== '' && editor.innerHTML.trim() !== '<br>') { if (editor.innerHTML.trim() !== '' && editor.innerHTML.trim() !== '<br>') {
clearEditor(); clearEditor();
@ -498,38 +498,38 @@ const sendMessage = () => {
} }
messageToSend.items.forEach(item => { messageToSend.items.forEach(item => {
if (item.type === 1 && cleanInvisibleChars(item.content.trimEnd())) { if (item.type === 1 && cleanInvisibleChars(item.content.trimEnd())) {
const data = { const data = {
items: [{ items: [{
content: cleanInvisibleChars(item.content), content: cleanInvisibleChars(item.content),
type: 1 type: 1
}], }],
mentionUids: messageToSend.mentionUids, mentionUids: messageToSend.mentionUids,
mentions: messageToSend.mentionUids.map(uid => { mentions: messageToSend.mentionUids.map(uid => {
return { return {
atid: uid, atid: uid,
name: mentionList.value.find(member => member.id === uid)?.nickname || '' name: mentionList.value.find(member => member.id === uid)?.nickname || ''
} }
}), }),
quoteId: messageToSend.quoteId, quoteId: messageToSend.quoteId,
} }
emit( emit(
'editor-event', 'editor-event',
emitCall('text_event', data) emitCall('text_event', data)
) )
} else if (item.type === 3) { } else if (item.type === 3) {
const data = { const data = {
height: 0, height: 0,
width: 0, width: 0,
size: 10000, size: 10000,
url: item.content, url: item.content,
} }
emit( emit(
'editor-event', 'editor-event',
emitCall('image_event', data) emitCall('image_event', data)
) )
} else if (item.type === 4) { } else if (item.type === 4) {
} }
}) })
clearEditor(); clearEditor();
} }
const parseEditorContent = () => { const parseEditorContent = () => {
@ -776,23 +776,23 @@ const onUploadSendImg = async (event) => {
console.error('Image upload failed or received invalid response:', res); console.error('Image upload failed or received invalid response:', res);
const previewImages = editorRef.value.querySelectorAll('img[data-status="local-preview"][src^="data:image"]:not([data-uploaded-url])'); const previewImages = editorRef.value.querySelectorAll('img[data-status="local-preview"][src^="data:image"]:not([data-uploaded-url])');
if (previewImages.length > 0) { if (previewImages.length > 0) {
const lastPreviewImage = previewImages[previewImages.length -1]; const lastPreviewImage = previewImages[previewImages.length - 1];
if(lastPreviewImage) { if (lastPreviewImage) {
lastPreviewImage.style.border = '2px dashed red'; lastPreviewImage.style.border = '2px dashed red';
lastPreviewImage.title = 'Upload failed'; lastPreviewImage.title = 'Upload failed';
} }
} }
} }
} catch (error) { } catch (error) {
console.error('Error during image upload process:', error); console.error('Error during image upload process:', error);
const previewImages = editorRef.value.querySelectorAll('img[data-status="local-preview"][src^="data:image"]:not([data-uploaded-url])'); const previewImages = editorRef.value.querySelectorAll('img[data-status="local-preview"][src^="data:image"]:not([data-uploaded-url])');
if (previewImages.length > 0) { if (previewImages.length > 0) {
const lastPreviewImage = previewImages[previewImages.length -1]; const lastPreviewImage = previewImages[previewImages.length - 1];
if(lastPreviewImage) { if (lastPreviewImage) {
lastPreviewImage.style.border = '2px dashed red'; lastPreviewImage.style.border = '2px dashed red';
lastPreviewImage.title = 'Upload error'; lastPreviewImage.title = 'Upload error';
}
} }
}
} }
} }
if (event.target) event.target.value = ''; if (event.target) event.target.value = '';
@ -927,17 +927,17 @@ const onSubscribeMention = async (data) => {
fallbackRange.selectNodeContents(editorNode); fallbackRange.selectNodeContents(editorNode);
fallbackRange.collapse(false); fallbackRange.collapse(false);
const newSelection = window.getSelection(); const newSelection = window.getSelection();
if (newSelection){ if (newSelection) {
newSelection.removeAllRanges(); newSelection.removeAllRanges();
newSelection.addRange(fallbackRange); newSelection.addRange(fallbackRange);
insertMention(data, fallbackRange); insertMention(data, fallbackRange);
} else { } else {
console.error("Could not get window selection to insert mention."); console.error("Could not get window selection to insert mention.");
} }
} }
}; };
const handleDeleteQuote = function(e) { const handleDeleteQuote = function (e) {
if (e.key !== 'Backspace' && e.key !== 'Delete') return; if (e.key !== 'Backspace' && e.key !== 'Delete') return;
const selection = window.getSelection(); const selection = window.getSelection();
if (selection.rangeCount === 0) return; if (selection.rangeCount === 0) return;
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
@ -945,23 +945,23 @@ const handleDeleteQuote = function(e) {
if (!editor) return; if (!editor) return;
const quoteElement = editor.querySelector('.editor-quote'); const quoteElement = editor.querySelector('.editor-quote');
if (!quoteElement) { if (!quoteElement) {
editor.removeEventListener('keydown', handleDeleteQuote); editor.removeEventListener('keydown', handleDeleteQuote);
return; return;
} }
const quoteIndex = Array.from(editor.childNodes).indexOf(quoteElement); const quoteIndex = Array.from(editor.childNodes).indexOf(quoteElement);
const isBeforeQuote = e.key === 'Backspace' && const isBeforeQuote = e.key === 'Backspace' &&
range.collapsed && range.collapsed &&
range.startContainer === editor && range.startContainer === editor &&
quoteIndex === range.startOffset; quoteIndex === range.startOffset;
const isAfterQuote = e.key === 'Delete' && const isAfterQuote = e.key === 'Delete' &&
range.collapsed && range.collapsed &&
range.startContainer === editor && range.startContainer === editor &&
quoteIndex === range.startOffset - 1; quoteIndex === range.startOffset - 1;
if (isBeforeQuote || isAfterQuote) { if (isBeforeQuote || isAfterQuote) {
e.preventDefault(); e.preventDefault();
quoteElement.remove(); quoteElement.remove();
quoteData.value = null; quoteData.value = null;
handleInput({ target: editor }); handleInput({ target: editor });
} }
}; };
const onSubscribeQuote = (data) => { const onSubscribeQuote = (data) => {
@ -974,7 +974,7 @@ const onSubscribeQuote = (data) => {
if (selection && selection.rangeCount > 0) { if (selection && selection.rangeCount > 0) {
const currentRange = selection.getRangeAt(0); const currentRange = selection.getRangeAt(0);
if (editor.contains(currentRange.commonAncestorContainer)) { if (editor.contains(currentRange.commonAncestorContainer)) {
savedRange = currentRange.cloneRange(); savedRange = currentRange.cloneRange();
} }
} }
const quoteElement = document.createElement('div'); const quoteElement = document.createElement('div');
@ -1014,11 +1014,11 @@ const onSubscribeQuote = (data) => {
let nodeToPlaceCursorAfter = quoteElement; let nodeToPlaceCursorAfter = quoteElement;
const zeroWidthSpace = document.createTextNode('\u200B'); const zeroWidthSpace = document.createTextNode('\u200B');
if (editor.lastChild === quoteElement || !quoteElement.nextSibling) { if (editor.lastChild === quoteElement || !quoteElement.nextSibling) {
editor.appendChild(zeroWidthSpace); editor.appendChild(zeroWidthSpace);
nodeToPlaceCursorAfter = zeroWidthSpace; nodeToPlaceCursorAfter = zeroWidthSpace;
} else { } else {
editor.insertBefore(zeroWidthSpace, quoteElement.nextSibling); editor.insertBefore(zeroWidthSpace, quoteElement.nextSibling);
nodeToPlaceCursorAfter = zeroWidthSpace; nodeToPlaceCursorAfter = zeroWidthSpace;
} }
const handleQuoteClick = (e) => { const handleQuoteClick = (e) => {
e.stopPropagation(); e.stopPropagation();
@ -1050,30 +1050,30 @@ const onSubscribeQuote = (data) => {
if (!newSelection) return; if (!newSelection) return;
let cursorPlaced = false; let cursorPlaced = false;
if (savedRange) { if (savedRange) {
try { try {
if (editor.contains(savedRange.commonAncestorContainer) && savedRange.startContainer) { if (editor.contains(savedRange.commonAncestorContainer) && savedRange.startContainer) {
newSelection.removeAllRanges(); newSelection.removeAllRanges();
newSelection.addRange(savedRange); newSelection.addRange(savedRange);
cursorPlaced = true; cursorPlaced = true;
}
} catch (err) {
} }
} catch (err) {
}
} }
if (!cursorPlaced) { if (!cursorPlaced) {
const newRange = document.createRange(); const newRange = document.createRange();
if (nodeToPlaceCursorAfter && nodeToPlaceCursorAfter.parentNode === editor) { if (nodeToPlaceCursorAfter && nodeToPlaceCursorAfter.parentNode === editor) {
newRange.setStartAfter(nodeToPlaceCursorAfter); newRange.setStartAfter(nodeToPlaceCursorAfter);
} else if (quoteElement.parentNode === editor && quoteElement.nextSibling) { } else if (quoteElement.parentNode === editor && quoteElement.nextSibling) {
newRange.setStartAfter(quoteElement.nextSibling); newRange.setStartAfter(quoteElement.nextSibling);
} else if (quoteElement.parentNode === editor) { } else if (quoteElement.parentNode === editor) {
newRange.setStartAfter(quoteElement); newRange.setStartAfter(quoteElement);
} else { } else {
newRange.selectNodeContents(editor); newRange.selectNodeContents(editor);
newRange.collapse(false); newRange.collapse(false);
} }
newRange.collapse(true); newRange.collapse(true);
newSelection.removeAllRanges(); newSelection.removeAllRanges();
newSelection.addRange(newRange); newSelection.addRange(newRange);
} }
editor.scrollTop = editor.scrollHeight; editor.scrollTop = editor.scrollHeight;
handleInput(); handleInput();
@ -1101,11 +1101,11 @@ const saveDraft = () => {
quoteElements.forEach(quote => quote.remove()) quoteElements.forEach(quote => quote.remove())
const contentToSave = tempDiv.textContent || '' const contentToSave = tempDiv.textContent || ''
const htmlToSave = tempDiv.innerHTML || '' const htmlToSave = tempDiv.innerHTML || ''
const currentEditor= parseEditorContent().items const currentEditor = parseEditorContent().items
const hasContent = contentToSave.trim().length > 0 || const hasContent = contentToSave.trim().length > 0 ||
htmlToSave.includes('<img') || htmlToSave.includes('<img') ||
htmlToSave.includes('editor-file') htmlToSave.includes('editor-file')
if (currentEditor.length>0) { if (currentEditor.length > 0) {
editorDraftStore.items[indexName.value] = JSON.stringify({ editorDraftStore.items[indexName.value] = JSON.stringify({
content: currentEditor.reduce((result, x) => { content: currentEditor.reduce((result, x) => {
if (x.type === 3) return result + '[图片]' if (x.type === 3) return result + '[图片]'
@ -1188,7 +1188,7 @@ const onCodeSubmit = (data) => {
emit('editor-event', { emit('editor-event', {
event: 'code_event', event: 'code_event',
data, data,
callBack: () => {} callBack: () => { }
}) })
isShowCode.value = false isShowCode.value = false
} }
@ -1196,7 +1196,7 @@ const onVoteSubmit = (data) => {
emit('editor-event', { emit('editor-event', {
event: 'vote_event', event: 'vote_event',
data, data,
callBack: () => {} callBack: () => { }
}) })
isShowVote.value = false isShowVote.value = false
} }
@ -1223,15 +1223,8 @@ const handleEditorClick = (event) => {
<section class="el-container is-vertical"> <section class="el-container is-vertical">
<header class="el-header toolbar bdr-t"> <header class="el-header toolbar bdr-t">
<div class="tools pr-30px"> <div class="tools pr-30px">
<n-popover <n-popover placement="top-start" trigger="click" raw :show-arrow="false" :width="300" ref="emoticonRef"
placement="top-start" style="width: 500px; height: 250px; border-radius: 10px; overflow: hidden">
trigger="click"
raw
:show-arrow="false"
:width="300"
ref="emoticonRef"
style="width: 500px; height: 250px; border-radius: 10px; overflow: hidden"
>
<template #trigger> <template #trigger>
<div class="item pointer"> <div class="item pointer">
<n-icon size="18" class="icon" :component="SmilingFace" /> <n-icon size="18" class="icon" :component="SmilingFace" />
@ -1240,52 +1233,30 @@ const handleEditorClick = (event) => {
</template> </template>
<MeEditorEmoticon @on-select="onEmoticonEvent" /> <MeEditorEmoticon @on-select="onEmoticonEvent" />
</n-popover> </n-popover>
<div <div class="item pointer" v-for="nav in navs" :key="nav.title" v-show="nav.show" @click="nav.click">
class="item pointer"
v-for="nav in navs"
:key="nav.title"
v-show="nav.show"
@click="nav.click"
>
<n-icon size="18" class="icon" :component="nav.icon" /> <n-icon size="18" class="icon" :component="nav.icon" />
<p class="tip-title">{{ nav.title }}</p> <p class="tip-title">{{ nav.title }}</p>
</div> </div>
<n-button class="w-80px h-30px ml-auto" type="primary" @click="sendMessage"> <n-button class="w-80px h-30px ml-auto" type="primary" @click="sendMessage">
<template #icon> <template #icon>
<n-icon> <n-icon>
<IosSend /> <IosSend />
</n-icon> </n-icon>
</template> </template>
发送 发送
</n-button> </n-button>
</div> </div>
</header> </header>
<main class="el-main height100"> <main class="el-main height100">
<div <div ref="editorRef" class="custom-editor" contenteditable="true" :placeholder="placeholder"
ref="editorRef" @input="handleInput" @keydown="handleKeydown" @paste="handlePaste" @focus="handleFocus" @blur="handleBlur">
class="custom-editor" </div>
contenteditable="true" <div v-if="showMention && dialogueStore.talk.talk_type === 2" class="mention-list py-5px"
:placeholder="placeholder" :style="{ top: mentionPosition.top + 'px', left: mentionPosition.left + 'px' }">
@input="handleInput"
@keydown="handleKeydown"
@paste="handlePaste"
@focus="handleFocus"
@blur="handleBlur"
></div>
<div
v-if="showMention && dialogueStore.talk.talk_type === 2"
class="mention-list py-5px"
:style="{ top: mentionPosition.top + 'px', left: mentionPosition.left + 'px' }"
>
<ul class="max-h-140px w-163px overflow-auto hide-scrollbar"> <ul class="max-h-140px w-163px overflow-auto hide-scrollbar">
<li <li v-for="(member, index) in mentionList" :key="member.user_id || member.id"
v-for="(member, index) in mentionList" class="cursor-pointer px-14px h-42px" :class="{ 'bg-#EEE9F9': index === selectedMentionIndex }"
:key="member.user_id || member.id" @mousedown.prevent="handleMentionSelectByMouse(member)" @mouseover="selectedMentionIndex = index">
class="cursor-pointer px-14px h-42px"
:class="{ 'bg-#EEE9F9': index === selectedMentionIndex }"
@mousedown.prevent="handleMentionSelectByMouse(member)"
@mouseover="selectedMentionIndex = index"
>
<div class="flex items-center border-b-1px border-b-solid border-b-#F8F8F8 h-full"> <div class="flex items-center border-b-1px border-b-solid border-b-#F8F8F8 h-full">
<img class="w-26px h-26px rounded-50% mr-11px" :src="member.avatar" alt=""> <img class="w-26px h-26px rounded-50% mr-11px" :src="member.avatar" alt="">
<span>{{ member.nickname }}</span> <span>{{ member.nickname }}</span>
@ -1305,14 +1276,17 @@ const handleEditorClick = (event) => {
.editor { .editor {
--tip-bg-color: rgb(241 241 241 / 90%); --tip-bg-color: rgb(241 241 241 / 90%);
height: 100%; height: 100%;
.toolbar { .toolbar {
height: 38px; height: 38px;
display: flex; display: flex;
.tools { .tools {
height: 40px; height: 40px;
flex: auto; flex: auto;
display: flex; display: flex;
align-items: center; align-items: center;
.item { .item {
display: flex; display: flex;
align-items: center; align-items: center;
@ -1321,6 +1295,7 @@ const handleEditorClick = (event) => {
margin: 0 2px; margin: 0 2px;
position: relative; position: relative;
user-select: none; user-select: none;
.tip-title { .tip-title {
display: none; display: none;
position: absolute; position: absolute;
@ -1337,6 +1312,7 @@ const handleEditorClick = (event) => {
user-select: none; user-select: none;
z-index: 999999999999; z-index: 999999999999;
} }
&:hover { &:hover {
.tip-title { .tip-title {
display: block; display: block;
@ -1345,6 +1321,7 @@ const handleEditorClick = (event) => {
} }
} }
} }
:deep(.editor-file) { :deep(.editor-file) {
display: inline-block; display: inline-block;
padding: 5px 10px; padding: 5px 10px;
@ -1361,6 +1338,7 @@ const handleEditorClick = (event) => {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
&::after { &::after {
content: attr(data-size); content: attr(data-size);
position: absolute; position: absolute;
@ -1368,10 +1346,12 @@ const handleEditorClick = (event) => {
color: #757575; color: #757575;
font-size: 12px; font-size: 12px;
} }
&:hover { &:hover {
background-color: #e3f2fd; background-color: #e3f2fd;
} }
} }
:deep(.editor-emoji) { :deep(.editor-emoji) {
display: inline-block; display: inline-block;
width: 24px; width: 24px;
@ -1379,6 +1359,7 @@ const handleEditorClick = (event) => {
vertical-align: middle; vertical-align: middle;
margin: 0 2px; margin: 0 2px;
} }
:deep(.editor-quote) { :deep(.editor-quote) {
margin-bottom: 8px; margin-bottom: 8px;
padding: 8px 12px; padding: 8px 12px;
@ -1394,18 +1375,22 @@ const handleEditorClick = (event) => {
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
&:hover { &:hover {
background-color: var(--im-message-left-bg-hover-color, #eaeaea); background-color: var(--im-message-left-bg-hover-color, #eaeaea);
} }
.quote-content-wrapper { .quote-content-wrapper {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
} }
.quote-title { .quote-title {
color: var(--im-primary-color, #409eff); color: var(--im-primary-color, #409eff);
margin-bottom: 4px; margin-bottom: 4px;
font-weight: 500; font-weight: 500;
} }
.quote-content { .quote-content {
color: var(--im-text-color, #333); color: var(--im-text-color, #333);
word-break: break-all; word-break: break-all;
@ -1416,12 +1401,14 @@ const handleEditorClick = (event) => {
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
.quote-image img { .quote-image img {
max-width: 100px; max-width: 100px;
max-height: 60px; max-height: 60px;
border-radius: 3px; border-radius: 3px;
pointer-events: none; pointer-events: none;
} }
.quote-close { .quote-close {
width: 18px; width: 18px;
height: 18px; height: 18px;
@ -1434,12 +1421,14 @@ const handleEditorClick = (event) => {
font-size: 16px; font-size: 16px;
margin-left: 8px; margin-left: 8px;
user-select: none; user-select: none;
&:hover { &:hover {
background-color: rgba(0, 0, 0, 0.2); background-color: rgba(0, 0, 0, 0.2);
color: #333; color: #333;
} }
} }
} }
.custom-editor { .custom-editor {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -1452,44 +1441,53 @@ const handleEditorClick = (event) => {
color: #333; color: #333;
background: transparent; background: transparent;
overflow-y: auto; overflow-y: auto;
&:empty:before { &:empty:before {
content: attr(placeholder); content: attr(placeholder);
color: #999; color: #999;
pointer-events: none; pointer-events: none;
} }
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 3px; width: 3px;
height: 3px; height: 3px;
background-color: unset; background-color: unset;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
border-radius: 3px; border-radius: 3px;
background-color: transparent; background-color: transparent;
} }
&:hover { &:hover {
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background-color: var(--im-scrollbar-thumb); background-color: var(--im-scrollbar-thumb);
} }
} }
} }
.custom-editor:empty::before { .custom-editor:empty::before {
content: attr(placeholder); content: attr(placeholder);
color: #999; color: #999;
pointer-events: none; pointer-events: none;
font-family: PingFang SC, Microsoft YaHei, 'Alibaba PuHuiTi 2.0 45' !important; font-family: PingFang SC, Microsoft YaHei, 'Alibaba PuHuiTi 2.0 45' !important;
} }
.custom-editor:focus { .custom-editor:focus {
outline: none; outline: none;
} }
.mention:hover { .mention:hover {
background-color: #bae7ff; background-color: #bae7ff;
} }
.editor-emoji { .editor-emoji {
width: 20px; width: 20px;
height: 20px; height: 20px;
vertical-align: middle; vertical-align: middle;
margin: 0 2px; margin: 0 2px;
} }
.mention-list { .mention-list {
position: absolute; position: absolute;
background-color: white; background-color: white;
@ -1498,6 +1496,7 @@ const handleEditorClick = (event) => {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 1000; z-index: 1000;
} }
.quote-card { .quote-card {
background: #f5f5f5; background: #f5f5f5;
border-left: 3px solid #1890ff; border-left: 3px solid #1890ff;
@ -1506,18 +1505,22 @@ const handleEditorClick = (event) => {
border-radius: 4px; border-radius: 4px;
position: relative; position: relative;
} }
.quote-content { .quote-content {
font-size: 12px; font-size: 12px;
} }
.quote-title { .quote-title {
font-weight: bold; font-weight: bold;
color: #1890ff; color: #1890ff;
margin-bottom: 4px; margin-bottom: 4px;
} }
.quote-text { .quote-text {
color: #666; color: #666;
line-height: 1.4; line-height: 1.4;
} }
.quote-close { .quote-close {
position: absolute; position: absolute;
top: 4px; top: 4px;
@ -1528,6 +1531,7 @@ const handleEditorClick = (event) => {
color: #999; color: #999;
font-size: 12px; font-size: 12px;
} }
.edit-tip { .edit-tip {
background: #fff7e6; background: #fff7e6;
border: 1px solid #ffd591; border: 1px solid #ffd591;
@ -1540,6 +1544,7 @@ const handleEditorClick = (event) => {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.edit-tip button { .edit-tip button {
background: none; background: none;
border: none; border: none;
@ -1548,11 +1553,13 @@ const handleEditorClick = (event) => {
margin-left: auto; margin-left: auto;
} }
} }
html[theme-mode='dark'] { html[theme-mode='dark'] {
.editor { .editor {
--tip-bg-color: #48484d; --tip-bg-color: #48484d;
} }
} }
:deep(.editor-image-wrapper.image-upload-loading::before) { :deep(.editor-image-wrapper.image-upload-loading::before) {
content: ''; content: '';
position: absolute; position: absolute;
@ -1568,19 +1575,23 @@ html[theme-mode='dark'] {
animation: spin 0.6s linear infinite; animation: spin 0.6s linear infinite;
z-index: 1; z-index: 1;
} }
:deep(.editor-image-wrapper.image-upload-loading img) { :deep(.editor-image-wrapper.image-upload-loading img) {
opacity: 0.5; opacity: 0.5;
} }
@keyframes spin { @keyframes spin {
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
.hide-scrollbar { .hide-scrollbar {
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 0; width: 0;
display: none; display: none;
} }
scrollbar-width: none; scrollbar-width: none;
-ms-overflow-style: none; -ms-overflow-style: none;
} }

View File

@ -227,14 +227,13 @@ class Talk extends Base {
}) })
}, 1000) }, 1000)
} }
console.log('输出加载1')
// 获取聊天面板元素节点 // 获取聊天面板元素节点
const el = document.getElementById('imChatPanel') const el = document.getElementById('imChatPanel')
if (!el) return if (!el) return
// 判断的滚动条是否在底部 // 判断的滚动条是否在底部
const isBottom = isScrollAtBottom(el) const isBottom = isScrollAtBottom(el)
if (isBottom || record.user_id == this.getAccountId()) { if (isBottom || record.user_id == this.getAccountId()) {
scrollToBottom() scrollToBottom()
} else { } else {

View File

@ -167,7 +167,7 @@ export const useTalkRecord = (uid: number) => {
nextTick(() => { nextTick(() => {
const el = document.getElementById('imChatPanel') const el = document.getElementById('imChatPanel')
console.log('request',request)
if (el) { if (el) {
if (request.cursor == 0) { if (request.cursor == 0) {
// el.scrollTop = el.scrollHeight // el.scrollTop = el.scrollHeight
@ -175,6 +175,12 @@ export const useTalkRecord = (uid: number) => {
// setTimeout(() => { // setTimeout(() => {
// el.scrollTop = el.scrollHeight + 1000 // el.scrollTop = el.scrollHeight + 1000
// }, 500) // }, 500)
console.log('滚动到底部')
// 在初次加载完成后恢复上传任务
// 确保在所有聊天记录加载完成后再恢复上传任务
dialogueStore.restoreUploadTasks()
scrollToBottom() scrollToBottom()
} else { } else {
el.scrollTop = el.scrollHeight - scrollHeight el.scrollTop = el.scrollHeight - scrollHeight
@ -322,6 +328,8 @@ export const useTalkRecord = (uid: number) => {
}) })
} else { } else {
// 其他情况滚动到底部 // 其他情况滚动到底部
// 在特殊参数模式下也需要恢复上传任务
dialogueStore.restoreUploadTasks()
scrollToBottom() scrollToBottom()
} }
} }
@ -332,6 +340,7 @@ export const useTalkRecord = (uid: number) => {
loadConfig.specialParams = undefined // 普通模式清空 loadConfig.specialParams = undefined // 普通模式清空
// 原有逻辑 // 原有逻辑
console.log('onLoad()执行load')
load(params) load(params)
} }
@ -369,6 +378,7 @@ export const useTalkRecord = (uid: number) => {
} else { } else {
// 如果不匹配,重置为普通模式 // 如果不匹配,重置为普通模式
resetLoadConfig() resetLoadConfig()
console.log('load执行2')
load({ load({
receiver_id: loadConfig.receiver_id, receiver_id: loadConfig.receiver_id,
talk_type: loadConfig.talk_type, talk_type: loadConfig.talk_type,
@ -377,6 +387,7 @@ export const useTalkRecord = (uid: number) => {
} }
} else { } else {
// 原有逻辑 // 原有逻辑
console.log('load执行3')
load({ load({
receiver_id: loadConfig.receiver_id, receiver_id: loadConfig.receiver_id,
talk_type: loadConfig.talk_type, talk_type: loadConfig.talk_type,

View File

@ -15,7 +15,9 @@ export const useDialogueStore = defineStore('dialogue', {
return { return {
// 对话索引(聊天对话的唯一索引) // 对话索引(聊天对话的唯一索引)
index_name: '', index_name: '',
globalUploadList:[],
// 添加一个映射,用于快速查找每个会话的上传任务
uploadTaskMap: {}, // 格式: { "talk_type_receiver_id": [task1, task2, ...] }
// 对话节点 // 对话节点
talk: { talk: {
avatar:'', avatar:'',
@ -129,8 +131,10 @@ export const useDialogueStore = defineStore('dialogue', {
if (data.talk_type == 2) { if (data.talk_type == 2) {
this.updateGroupMembers() this.updateGroupMembers()
this.getGroupInfo() this.getGroupInfo()
} }
// 注意:上传任务的恢复将在聊天记录加载完成后进行
// 在useTalkRecord.ts的onLoad方法中会在加载完聊天记录后调用restoreUploadTasks方法
}, },
// 更新提及列表 // 更新提及列表
@ -292,6 +296,16 @@ export const useDialogueStore = defineStore('dialogue', {
// 更新视频上传进度 // 更新视频上传进度
updateUploadProgress(uploadId, percentage) { updateUploadProgress(uploadId, percentage) {
// 更新全局列表中的进度
const globalTask = this.globalUploadList.find(item =>
item.extra && item.extra.is_uploading && item.extra.upload_id === uploadId
)
if (globalTask) {
globalTask.extra.percentage = percentage
}
// 更新当前会话记录中的进度
const record = this.records.find(item => const record = this.records.find(item =>
item.extra && item.extra.is_uploading && item.extra.upload_id === uploadId item.extra && item.extra.is_uploading && item.extra.upload_id === uploadId
) )
@ -301,6 +315,44 @@ export const useDialogueStore = defineStore('dialogue', {
} }
}, },
// 添加上传任务
addUploadTask(task) {
// 添加到全局列表
this.globalUploadList.push(task)
// 添加到会话映射
const sessionKey = `${task.talk_type}_${task.receiver_id}`
if (!this.uploadTaskMap[sessionKey]) {
this.uploadTaskMap[sessionKey] = []
}
this.uploadTaskMap[sessionKey].push(task)
// 同时添加到当前会话记录
this.addDialogueRecord(task)
},
// 上传完成后移除任务
removeUploadTask(uploadId) {
// 从全局列表中找到任务
const taskIndex = this.globalUploadList.findIndex(item => item.msg_id === uploadId)
if (taskIndex >= 0) {
const task = this.globalUploadList[taskIndex]
const sessionKey = `${task.talk_type}_${task.receiver_id}`
// 从会话映射中移除
if (this.uploadTaskMap[sessionKey]) {
const mapIndex = this.uploadTaskMap[sessionKey].findIndex(item => item.msg_id === uploadId)
if (mapIndex >= 0) {
this.uploadTaskMap[sessionKey].splice(mapIndex, 1)
}
}
// 从全局列表中移除
this.globalUploadList.splice(taskIndex, 1)
}
},
// 视频上传完成后更新消息 // 视频上传完成后更新消息
completeUpload(uploadId, videoInfo) { completeUpload(uploadId, videoInfo) {
const record = this.records.find(item => const record = this.records.find(item =>
@ -317,6 +369,23 @@ export const useDialogueStore = defineStore('dialogue', {
// 更新会话信息 // 更新会话信息
updateDialogueTalk(params){ updateDialogueTalk(params){
Object.assign(this.talk, params) Object.assign(this.talk, params)
},
// 恢复当前会话的上传任务
restoreUploadTasks() {
// 获取当前会话的sessionKey
const sessionKey = `${this.talk.talk_type}_${this.talk.receiver_id}`
// 检查是否有需要恢复的上传任务
if (this.uploadTaskMap[sessionKey] && this.uploadTaskMap[sessionKey].length > 0) {
// 按照插入顺序排序上传任务
const tasks = [...this.uploadTaskMap[sessionKey]].sort((a, b) => a.insert_sequence - b.insert_sequence)
// 将上传任务添加到当前会话记录中
tasks.forEach(task => {
this.addDialogueRecord(task)
})
}
} }
} }
}) })

View File

@ -1,7 +1,13 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ServeFindFileSplitInfo, ServeFileSubareaUpload } from '@/api/upload' // import { message } from 'naive-ui'
import { ServeSendTalkFile } from '@/api/chat' import {
import { uploadImg } from '@/api/upload' ServeSendTalkFile
} from '@/api/chat'
import {
uploadImg,
ServeFindFileSplitInfo,
ServeFileSubareaUpload
} from '@/api/upload'
import { import {
useDialogueStore useDialogueStore
} from '@/store' } from '@/store'
@ -140,12 +146,12 @@ export const useUploadsStore = defineStore('uploads', {
this.triggerUpload(upload_id, clientUploadId) this.triggerUpload(upload_id, clientUploadId)
} else { } else {
message.error(res.message) message.error(res.message)
onProgress(-1) // 通知上传失败 this.handleUploadError(upload_id, clientUploadId)
} }
} catch (error) { } catch (error) {
console.error("初始化分片上传失败:", error); console.error("初始化分片上传失败:", error);
message.error("初始化上传失败,请重试") message.error("初始化上传失败,请重试")
onProgress(-1) this.handleUploadError(upload_id, clientUploadId)
} }
}, },
@ -201,26 +207,20 @@ export const useUploadsStore = defineStore('uploads', {
this.triggerUpload(uploadId, clientUploadId) this.triggerUpload(uploadId, clientUploadId)
} }
} else { } else {
updatedItem.onProgress(-1)
// 上传失败处理 // 上传失败处理
console.error(`分片上传失败,错误码: ${res.code},错误信息: ${res.message || '未知错误'}`); console.error(`分片上传失败,错误码: ${res.code},错误信息: ${res.message || '未知错误'}`);
updatedItem.status = 3 this.handleUploadError(uploadId, clientUploadId || '')
} }
} catch (error) { } catch (error) {
updatedItem.onProgress(-1)
console.error("分片上传错误:", error); console.error("分片上传错误:", error);
// 获取最新的项目状态 // 获取最新的项目状态
// 这里不应该重新定义变量而是使用已有的updatedItem
// const updatedItem = this.findItem(uploadId)
if (!updatedItem) return if (!updatedItem) return
// 如果是暂停导致的错误,不改变状态 // 如果是暂停导致的错误,不改变状态
if (updatedItem.is_paused) return if (updatedItem.is_paused) return
updatedItem.status = 3 this.handleUploadError(uploadId, clientUploadId || '')
} }
}, },
@ -244,6 +244,10 @@ export const useUploadsStore = defineStore('uploads', {
talk_type: item.talk_type talk_type: item.talk_type
}) })
// 从DialogueStore中移除上传任务
const dialogueStore = useDialogueStore()
dialogueStore.removeUploadTask(clientUploadId)
if (item.onComplete) { if (item.onComplete) {
item.onComplete(item) item.onComplete(item)
} }
@ -291,5 +295,21 @@ export const useUploadsStore = defineStore('uploads', {
// 从上传列表中移除旧的上传项 // 从上传列表中移除旧的上传项
this.items = this.items.filter(i => i.client_upload_id !== clientUploadId) this.items = this.items.filter(i => i.client_upload_id !== clientUploadId)
}, },
// 上传失败处理
async handleUploadError(uploadId: string, clientUploadId: string) {
const item = this.findItem(uploadId)
if (!item) return
item.status = 3 // 设置为上传失败状态
// 从DialogueStore中移除上传任务
const dialogueStore = useDialogueStore()
dialogueStore.removeUploadTask(clientUploadId)
if (item.onProgress) {
item.onProgress(-1) // 通知上传失败
}
}
} }
}) })

View File

@ -54,7 +54,6 @@ request.interceptors.request.use((config) => {
// 响应拦截器 // 响应拦截器
request.interceptors.response.use((response) => { request.interceptors.response.use((response) => {
console.log('response.data.status',response.data.status)
if(response.data.code !==200&&response.data.status!==0){ if(response.data.code !==200&&response.data.status!==0){
window['$message'].warning(response.data.msg) window['$message'].warning(response.data.msg)
} }

View File

@ -592,8 +592,6 @@ const indexName = computed(() => dialogueStore.index_name)
// //
const onTabTalk = (item: ISession, follow = false) => { const onTabTalk = (item: ISession, follow = false) => {
console.log('onTabTalk')
if (item.index_name === indexName.value) return if (item.index_name === indexName.value) return
searchKeyword.value = '' searchKeyword.value = ''
@ -638,7 +636,7 @@ const onReload = () => {
// //
const onInitialize = () => { const onInitialize = () => {
let index_name = getCacheIndexName() let index_name = getCacheIndexName()
console.log('index_name',index_name)
index_name && onTabTalk(talkStore.findItem(index_name), true) index_name && onTabTalk(talkStore.findItem(index_name), true)
} }

View File

@ -393,7 +393,7 @@ watch(
}, 3000) }, 3000)
return return
} }
console.log('执行逻辑')
onLoad( onLoad(
{ {
receiver_id: newProps.receiver_id, receiver_id: newProps.receiver_id,
@ -403,7 +403,7 @@ watch(
specialParams ? { specifiedMsg: specialParams } : undefined specialParams ? { specifiedMsg: specialParams } : undefined
) )
}, },
{ immediate: true, deep: true } { deep: true }
) )
// onMounted(() => { // onMounted(() => {
@ -589,12 +589,25 @@ const handleIntersection = (entries) => {
watch( watch(
() => records.value, () => records.value,
() => { () => {
console.log()
nextTick(() => { nextTick(() => {
// //
if (observer) { if (observer) {
observer.disconnect() observer.disconnect()
} }
//
// const recordsCopy = [...dialogueStore.records];
// for (const [y, iy] of dialogueStore.globalUploadList.entries()) {
// console.log('y',y)
// console.log('iy',iy)
// for (const [x, ix] of recordsCopy.entries()) {
// if(x.msg_id === y.pre_msg){
// // ix
// dialogueStore.records.splice(ix + 1 + (dialogueStore.records.length - recordsCopy.length), 0, y);
// }
// }
// }
// console.log('dialogueStore.records',dialogueStore.records)
// //
const options = { const options = {
root: null, root: null,

View File

@ -115,6 +115,9 @@ const onSendVideoEvent = async ({ data }) => {
// //
const tempMessage = { const tempMessage = {
msg_id: uploadId, msg_id: uploadId,
insert_sequence: dialogueStore.records.length > 0
? dialogueStore.records[dialogueStore.records.length-1].sequence
: 0,
sequence: Date.now(), sequence: Date.now(),
talk_type: props.talk_type, talk_type: props.talk_type,
msg_type: 5, // msg_type: 5, //
@ -137,8 +140,8 @@ const onSendVideoEvent = async ({ data }) => {
float: 'right' // float: 'right' //
} }
// // 使
dialogueStore.addDialogueRecord(tempMessage) dialogueStore.addUploadTask(tempMessage)
nextTick(()=>{ nextTick(()=>{
scrollToBottom() scrollToBottom()
}) })
@ -151,8 +154,8 @@ const onSendVideoEvent = async ({ data }) => {
dialogueStore.updateUploadProgress(uploadId, percentage) dialogueStore.updateUploadProgress(uploadId, percentage)
}, },
async () => { async () => {
dialogueStore.batchDelDialogueRecord([uploadId]) // removeUploadTask
// globalUploadList
} }
) )
} }
@ -171,6 +174,9 @@ const onSendFileEvent = ({ data }) => {
const clientUploadId = `file-${Date.now()}-${Math.floor(Math.random() * 1000)}` const clientUploadId = `file-${Date.now()}-${Math.floor(Math.random() * 1000)}`
const tempMessage = { const tempMessage = {
msg_id: clientUploadId, msg_id: clientUploadId,
insert_sequence: dialogueStore.records.length > 0
? dialogueStore.records[dialogueStore.records.length-1].sequence
: 0,
sequence: Date.now(), sequence: Date.now(),
talk_type: props.talk_type, talk_type: props.talk_type,
msg_type: 6, msg_type: 6,
@ -192,7 +198,7 @@ const onSendFileEvent = ({ data }) => {
}, },
float: 'right' float: 'right'
} }
dialogueStore.addDialogueRecord(tempMessage) dialogueStore.addUploadTask(tempMessage)
nextTick(()=>{ nextTick(()=>{
scrollToBottom() scrollToBottom()
}) })
@ -201,8 +207,8 @@ const onSendFileEvent = ({ data }) => {
dialogueStore.updateUploadProgress(clientUploadId, percentage) dialogueStore.updateUploadProgress(clientUploadId, percentage)
}, },
async () => { async () => {
dialogueStore.batchDelDialogueRecord([clientUploadId]) // removeUploadTask
// records
} }
) )
} }

View File

@ -46,9 +46,9 @@ export default defineConfig(({ mode }) => {
vueJsx({}), vueJsx({}),
compressPlugin(), compressPlugin(),
UnoCSS(), UnoCSS(),
// vueDevTools({ vueDevTools({
// launchEditor: 'trae', launchEditor: 'trae',
// }) })
], ],
define: { define: {
__APP_ENV__: env.APP_ENV __APP_ENV__: env.APP_ENV