diff --git a/src/components/editor/CustomEditor.vue b/src/components/editor/CustomEditor.vue
index 5dcd1ad..4b18196 100644
--- a/src/components/editor/CustomEditor.vue
+++ b/src/components/editor/CustomEditor.vue
@@ -101,49 +101,34 @@ const handleInput = (event) => {
// console.warn('handleInput called without a valid editor node.');
return;
}
- const target = editorNode; // Keep target for existing logic if it's deeply coupled, or refactor to use editorNode directly
-
+ const target = editorNode;
const editorClone = editorNode.cloneNode(true);
- // 优化:移除引用元素,只执行一次
- editorClone.querySelectorAll('.editor-quote').forEach(quote => quote.remove());
+ editorClone.querySelectorAll('.editor-quote').forEach(quote => quote.remove());
- // 提取文本内容,包括表情的alt文本
- // 注意:原逻辑中 textContent += altText 可能导致重复,因为 editorClone.textContent 可能已包含表情图片的文本(如果浏览器这样处理)
- // 一个更可靠的方法是遍历子节点,或者在移除表情图片后再获取textContent
- let rawTextContent = editorClone.textContent || '';
+ let rawTextContent = editorClone.textContent || '';
const emojiImages = editorClone.querySelectorAll('img.editor-emoji');
- // 暂时保留原提取方式,但标记为待优化
- // TODO: 优化 editorContent.value 的准确性,避免重复计算表情文本
- if (emojiImages.length > 0) {
+ if (emojiImages.length > 0) {
emojiImages.forEach(emoji => {
const altText = emoji.getAttribute('alt');
if (altText) {
- // 这里的拼接逻辑可能不完全准确,取决于textContent如何处理img的alt
- rawTextContent += altText;
+ rawTextContent += altText;
}
});
}
editorContent.value = rawTextContent;
- // const editorNode = target; // Already defined as editorNode
- const currentText = editorNode.textContent.trim();
+ const currentText = editorNode.textContent.trim();
const hasSpecialElements = editorNode.querySelector('img, .editor-file, .mention');
- // 优化:清空编辑器内容的逻辑
- // 如果编辑器内没有可见的文本内容,也没有图片、文件、提及等特殊元素,则尝试清空。
- if (currentText === '' && !hasSpecialElements) {
- // If the editor is visually empty (no text, no special elements),
- // ensure its innerHTML is cleared to allow the placeholder to show.
- // This handles cases where the browser might leave a
tag or other empty structures like
.
- if (editorNode.innerHTML !== '') {
+ if (currentText === '' && !hasSpecialElements) {
+ if (editorNode.innerHTML !== '') {
editorNode.innerHTML = '';
}
}
editorHtml.value = editorNode.innerHTML || '';
- // TODO: parseEditorContent, saveDraft, emit input_event 考虑使用防抖 (debounce)
- const currentEditorItems = parseEditorContent().items;
+ const currentEditorItems = parseEditorContent().items;
checkMention(target);
saveDraft();
@@ -153,8 +138,7 @@ const handleInput = (event) => {
data: currentEditorItems.reduce((result, item) => {
if (item.type === 1) return result + item.content;
if (item.type === 3) return result + '[图片]';
- // TODO: 为其他消息类型(如文件)添加文本表示
- return result;
+ return result;
}, '')
});
};
@@ -239,8 +223,7 @@ const insertMention = (member, clonedRange) => {
const offset = range.startOffset;
const textContent = textNode.nodeType === Node.TEXT_NODE ? textNode.textContent || '' : '';
- // 查找光标前最后一个 '@' 符号的位置
- const atIndex = (textNode.nodeType === Node.TEXT_NODE && offset > 0) ? textContent.lastIndexOf('@', offset - 1) : -1;
+ const atIndex = (textNode.nodeType === Node.TEXT_NODE && offset > 0) ? textContent.lastIndexOf('@', offset - 1) : -1;
const mentionSpan = document.createElement('span');
mentionSpan.className = 'mention';
@@ -248,28 +231,20 @@ const insertMention = (member, clonedRange) => {
mentionSpan.textContent = `@${member.value || member.nickname} `;
mentionSpan.contentEditable = 'false';
- // 如果找到了 '@' 符号,并且它在当前文本节点内
- if (atIndex !== -1 && textNode.nodeType === Node.TEXT_NODE) {
+ if (atIndex !== -1 && textNode.nodeType === Node.TEXT_NODE) {
const parent = textNode.parentNode;
if (!parent) return;
- // 设置范围以选中从 '@' 到当前光标位置的文本
- range.setStart(textNode, atIndex);
+ range.setStart(textNode, atIndex);
range.setEnd(textNode, offset);
- range.deleteContents(); // 删除选中的文本 (即 '@' 和查询词)
-
- range.insertNode(mentionSpan); // 插入提及元素
- } else {
- // 如果没有找到 '@' 或者不在当前文本节点,直接在光标处插入
- if (!range.collapsed) {
- range.deleteContents(); // 如果有选中文本,先删除
- }
+ range.deleteContents();
+ range.insertNode(mentionSpan); } else {
+ if (!range.collapsed) {
+ range.deleteContents(); }
range.insertNode(mentionSpan);
}
- // 直接将光标设置在提及元素后面,不使用零宽空格
- // 这样可以避免需要按两次删除键才能删除提及元素的问题
- range.setStartAfter(mentionSpan);
+ range.setStartAfter(mentionSpan);
range.collapse(true);
selection.removeAllRanges();
@@ -305,33 +280,26 @@ const handlePaste = (event) => {
image.src = tempUrl;
image.onload = () => {
- URL.revokeObjectURL(tempUrl); // 及时释放对象URL
- const form = new FormData();
+ URL.revokeObjectURL(tempUrl); const form = new FormData();
form.append('file', file);
form.append('source', 'fonchain-chat');
form.append('urlParam', `width=${image.width}&height=${image.height}`);
- // 先插入临时图片
- insertImage(tempUrl, image.width, image.height);
+ insertImage(tempUrl, image.width, image.height);
uploadImg(form).then(({ code, data, message }) => {
if (code === 0 && data && data.ori_url) {
- // 查找编辑器中对应的临时图片并替换其src
- const editorImages = editorRef.value.querySelectorAll('img.editor-image');
- // 从后向前查找,因为粘贴的图片通常是最后一个
- for (let j = editorImages.length - 1; j >= 0; j--) {
+ const editorImages = editorRef.value.querySelectorAll('img.editor-image');
+ for (let j = editorImages.length - 1; j >= 0; j--) {
if (editorImages[j].src === tempUrl) {
editorImages[j].src = data.ori_url;
- // 可选:更新图片的data属性,如果需要的话
- // editorImages[j].setAttribute('data-remote-url', data.ori_url);
+ // editorImages[j].setAttribute('data-remote-url', data.ori_url);
break;
}
}
- handleInput({ target: editorRef.value }); // 更新编辑器状态
- } else {
+ handleInput({ target: editorRef.value }); } else {
window['$message'].error(message || '图片上传失败');
- // 可选:如果上传失败,移除临时图片或显示错误提示
- const editorImages = editorRef.value.querySelectorAll('img.editor-image');
+ const editorImages = editorRef.value.querySelectorAll('img.editor-image');
for (let j = editorImages.length - 1; j >= 0; j--) {
if (editorImages[j].src === tempUrl) {
editorImages[j].remove();
@@ -343,8 +311,7 @@ const handlePaste = (event) => {
}).catch(error => {
console.error('Upload image error:', error);
window['$message'].error('图片上传过程中发生错误');
- // 清理临时图片
- const editorImages = editorRef.value.querySelectorAll('img.editor-image');
+ const editorImages = editorRef.value.querySelectorAll('img.editor-image');
for (let j = editorImages.length - 1; j >= 0; j--) {
if (editorImages[j].src === tempUrl) {
editorImages[j].remove();
@@ -358,14 +325,12 @@ const handlePaste = (event) => {
URL.revokeObjectURL(tempUrl);
window['$message'].error('无法加载粘贴的图片');
};
- return; // 处理完第一个图片就返回
- }
+ return; }
}
}
}
- // 如果没有粘贴图片,则处理文本
- if (!imagePasted) {
+ if (!imagePasted) {
const text = clipboardData.getData('text/plain') || '';
if (text) {
const selection = window.getSelection();
@@ -374,8 +339,7 @@ const handlePaste = (event) => {
range.deleteContents();
const textNode = document.createTextNode(text);
range.insertNode(textNode);
- // 将光标移到插入文本的末尾
- range.setStartAfter(textNode);
+ range.setStartAfter(textNode);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
@@ -415,8 +379,7 @@ const handleKeydown = (event) => {
const editor = editorRef.value;
if (!editor) return;
- // 提及列表相关操作
- if (showMention.value) {
+ if (showMention.value) {
const mentionUl = document.querySelector('.mention-list ul');
let handled = false;
switch (event.key) {
@@ -466,57 +429,46 @@ const handleKeydown = (event) => {
}
}
- // 删除提及元素 (@mention)
- if (event.key === 'Backspace' || event.key === 'Delete') {
+ if (event.key === 'Backspace' || event.key === 'Delete') {
const selection = window.getSelection();
if (!selection || !selection.rangeCount) return;
const range = selection.getRangeAt(0);
if (range.collapsed) {
let nodeToCheck = null;
- let positionRelativeToCheck = ''; // 'before' or 'after'
-
+ let positionRelativeToCheck = '';
const container = range.startContainer;
const offset = range.startOffset;
if (event.key === 'Backspace') {
- if (offset === 0) { // 光标在节点开头
- nodeToCheck = container.previousSibling;
+ if (offset === 0) { nodeToCheck = container.previousSibling;
positionRelativeToCheck = 'before';
} else if (container.nodeType === Node.ELEMENT_NODE && offset > 0) {
- // 光标在元素节点内,检查前一个子节点
- nodeToCheck = container.childNodes[offset - 1];
+ nodeToCheck = container.childNodes[offset - 1];
positionRelativeToCheck = 'before';
}
} else if (event.key === 'Delete') {
- if (container.nodeType === Node.TEXT_NODE && offset === container.textContent.length) { // 光标在文本节点末尾
- nodeToCheck = container.nextSibling;
+ if (container.nodeType === Node.TEXT_NODE && offset === container.textContent.length) { nodeToCheck = container.nextSibling;
positionRelativeToCheck = 'after';
} else if (container.nodeType === Node.ELEMENT_NODE && offset < container.childNodes.length) {
- // 光标在元素节点内,检查当前子节点(或下一个,取决于如何定义删除)
- nodeToCheck = container.childNodes[offset];
+ nodeToCheck = container.childNodes[offset];
positionRelativeToCheck = 'after';
}
}
- // 确保 nodeToCheck 是一个元素节点并且是 mention
- if (nodeToCheck && nodeToCheck.nodeType === Node.ELEMENT_NODE && nodeToCheck.classList.contains('mention')) {
+ if (nodeToCheck && nodeToCheck.nodeType === Node.ELEMENT_NODE && nodeToCheck.classList.contains('mention')) {
event.preventDefault();
const parent = nodeToCheck.parentNode;
parent.removeChild(nodeToCheck);
- // 不再需要检查和删除零宽空格,因为我们已经不使用零宽空格了
-
+
handleInput({ target: editor });
return;
}
}
- // 如果选区不折叠(即选中了内容),并且选区包含了mention,默认行为通常能正确处理,无需特殊干预
- // 但如果需要更精细的控制,例如确保整个mention被删除,则需要额外逻辑
- }
+ }
- // 处理换行 (Ctrl/Meta/Shift + Enter)
- if (event.key === 'Enter' && (event.ctrlKey || event.metaKey || event.shiftKey)) {
+ if (event.key === 'Enter' && (event.ctrlKey || event.metaKey || event.shiftKey)) {
event.preventDefault();
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
@@ -533,8 +485,7 @@ const handleKeydown = (event) => {
return;
}
- // 处理发送消息 (Enter)
- if (event.key === 'Enter' && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
+ if (event.key === 'Enter' && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
event.preventDefault();
const messageData = parseEditorContent();
const isEmptyMessage = messageData.items.length === 0 ||
@@ -572,17 +523,14 @@ const sendMessage = () => {
let finalItems = [];
if (parsedData && parsedData.items) {
finalItems = parsedData.items.map(item => {
- if (item.type === 1 && typeof item.content === 'string') { // 文本类型
- let content = cleanInvisibleChars(item.content);
+ if (item.type === 1 && typeof item.content === 'string') { let content = cleanInvisibleChars(item.content);
content = content.replace(/
/gi, '\n').trim();
return { ...item, content };
}
return item;
}).filter(item => {
if (item.type === 1 && !item.content && !(parsedData.mentionUids && parsedData.mentionUids.length > 0)) return false;
- if (item.type === 3 && !item.content) return false; // 图片
- if (item.type === 4 && !item.content) return false; // 文件
- return true;
+ if (item.type === 3 && !item.content) return false; if (item.type === 4 && !item.content) return false; return true;
});
}
@@ -607,14 +555,9 @@ const sendMessage = () => {
if (messageToSend.quoteId && quoteData.value && quoteData.value.id === messageToSend.quoteId) {
messageToSend.quote = { ...quoteData.value };
} else if (messageToSend.quoteId) {
- console.warn('sendMessage: Quote ID from parsed content exists, but no matching quoteData.value or ID mismatch.');
- // Decide if sending without full quote object is acceptable or if quoteId should be removed
- // For now, we keep quoteId but messageToSend.quote will not be populated with full details
} else {
- delete messageToSend.quote; // No valid quoteId, so no quote object
+ delete messageToSend.quote;
}
-
- // Determine event type based on content
const isSingleImageNoQuote = messageToSend.items.length === 1 && messageToSend.items[0].type === 3 && !messageToSend.quote;
const isSingleFileNoQuote = messageToSend.items.length === 1 && messageToSend.items[0].type === 4 && !messageToSend.quote;
@@ -627,16 +570,13 @@ const sendMessage = () => {
size: imgItem.size || 0
}));
} else if (isSingleFileNoQuote) {
- // Assuming a 'file_event' or similar for single files
- // If not, this will also go to 'text_event'
const fileItem = messageToSend.items[0];
- emit('editor-event', emitCall('file_event', { // Placeholder for actual file event
+ emit('editor-event', emitCall('file_event', {
url: fileItem.content,
name: fileItem.name,
size: fileItem.size
}));
} else {
- // All other cases: text, mixed content, or items with quotes
emit('editor-event', emitCall('text_event', messageToSend));
}
@@ -655,19 +595,17 @@ const parseEditorContent = () => {
}
const tempDiv = document.createElement('div');
- tempDiv.innerHTML = editorHtml.value; // Use editorHtml.value as the source of truth for parsing
+ tempDiv.innerHTML = editorHtml.value;
const quoteElement = tempDiv.querySelector('.editor-quote');
if (quoteElement && quoteData.value && quoteData.value.id) {
parsedQuoteId = quoteData.value.id;
- quoteElement.remove(); // Remove from tempDiv to avoid parsing its content
+ quoteElement.remove();
}
let currentTextBuffer = '';
const flushTextBufferIfNeeded = () => {
- // Only push non-empty text or if it's part of a larger structure (e.g. before an image)
- // Actual trimming and empty checks will be done in sendMessage
if (currentTextBuffer) {
items.push({ type: 1, content: currentTextBuffer });
}
@@ -684,38 +622,36 @@ const parseEditorContent = () => {
switch (node.tagName) {
case 'BR':
- currentTextBuffer += '\n'; // Represent
as newline in text content
+ currentTextBuffer += '\n';
break;
case 'IMG':
flushTextBufferIfNeeded();
const src = node.getAttribute('src');
const alt = node.getAttribute('alt');
const isEmojiPic = node.classList.contains('editor-emoji');
- const isTextEmojiPlaceholder = node.classList.contains('emoji'); // e.g.
+ const isTextEmojiPlaceholder = node.classList.contains('emoji');
if (isTextEmojiPlaceholder && alt) {
- currentTextBuffer += alt; // Treat as text
+ currentTextBuffer += alt;
} else if (src) {
items.push({
- type: 3, // Image
+ type: 3,
content: src,
isEmoji: isEmojiPic,
width: node.getAttribute('data-original-width') || node.width || null,
height: node.getAttribute('data-original-height') || node.height || null,
- // size: node.getAttribute('data-size') || null, // If available
+
});
}
break;
default:
if (node.classList.contains('mention')) {
- // Mentions are complex: they are part of text flow but also carry data.
- // Here, we add their text to buffer and collect UID.
- // The sendMessage function will construct the 'mentions' array.
+
const userId = node.getAttribute('data-user-id');
if (userId) {
mentionUids.add(Number(userId));
}
- currentTextBuffer += node.textContent || ''; // Add mention text to buffer
+ currentTextBuffer += node.textContent || '';
} else if (node.classList.contains('editor-file')) {
flushTextBufferIfNeeded();
const fileUrl = node.getAttribute('data-url');
@@ -723,7 +659,7 @@ const parseEditorContent = () => {
const fileSize = node.getAttribute('data-size-raw') || node.getAttribute('data-size') || 0;
if (fileUrl && fileName) {
items.push({
- type: 4, // File
+ type: 4,
content: fileUrl,
name: fileName,
size: parseInt(fileSize, 10),
@@ -739,7 +675,7 @@ const parseEditorContent = () => {
};
Array.from(tempDiv.childNodes).forEach(processNodeRecursively);
- flushTextBufferIfNeeded(); // Final flush for any remaining text
+ flushTextBufferIfNeeded();
return {
items: items.length > 0 ? items : [{ type: 1, content: '' }],
@@ -757,33 +693,24 @@ const clearEditor = () => {
editorHtml.value = '';
quoteData.value = null;
- // Reset mention related states
- hideMentionList(); // This already handles showMention, mentionList, currentMentionQuery
-
- // Remove quote element from the DOM if it exists within the editor
+
+ hideMentionList();
const existingQuoteElement = editorRef.value ? editorRef.value.querySelector('.editor-quote') : null;
if (existingQuoteElement) {
existingQuoteElement.remove();
}
- // saveDraft(); // Consider if saveDraft should be called. Clearing usually means discarding.
- // If draft should be cleared, it might be better to explicitly clear it:
- // localStorage.removeItem('editorDraft'); // Example
+ handleInput();
- // Trigger input event to update any listeners and ensure consistent state
- handleInput(); // This will update editorHtml based on (now empty) editorRef.value.innerHTML
-
- // Emit a specific clear event or ensure input_event with empty data is sufficient
emit('editor-event', {
- event: 'clear_event', // Or stick to 'input_event' if that's the convention
+ event: 'clear_event',
data: ''
});
if (editorRef.value) {
nextTick(() => {
editorRef.value.focus();
- // Ensure focus后编辑器仍然是空的,以保证placeholder显示
- if (editorRef.value && editorRef.value.innerHTML.toLowerCase() === '
') {
+ if (editorRef.value && editorRef.value.innerHTML.toLowerCase() === '
') {
editorRef.value.innerHTML = '';
}
});
@@ -797,10 +724,10 @@ const insertImage = (fileOrSrc, isUploaded = false, uploadedUrl = '') => {
if (!editorRef.value) return;
const img = document.createElement('img');
- img.className = 'editor-image'; // Keep existing class if it's styled
- img.alt = '图片'; // Default alt text
- img.style.maxWidth = '200px'; // Standardized max width
- img.style.maxHeight = '200px'; // Standardized max height
+ img.className = 'editor-image';
+ img.alt = '图片';
+ img.style.maxWidth = '200px';
+ img.style.maxHeight = '200px';
img.style.borderRadius = '4px';
img.style.objectFit = 'contain';
img.style.margin = '5px';
@@ -825,20 +752,18 @@ const insertImage = (fileOrSrc, isUploaded = false, uploadedUrl = '') => {
editorRef.value.focus();
range = document.createRange();
range.selectNodeContents(editorRef.value);
- range.collapse(false); // End of editor
+ range.collapse(false);
}
} else {
editorRef.value.focus();
range = document.createRange();
range.selectNodeContents(editorRef.value);
- range.collapse(false); // End of editor
+ range.collapse(false);
}
range.deleteContents();
range.insertNode(img);
-
- // Add a space after the image for better typing experience
- const spaceNode = document.createTextNode('\u00A0'); // Non-breaking space
+ const spaceNode = document.createTextNode('\u00A0');
range.insertNode(spaceNode);
range.setStartAfter(spaceNode);
range.collapse(true);
@@ -846,20 +771,20 @@ const insertImage = (fileOrSrc, isUploaded = false, uploadedUrl = '') => {
selection.addRange(range);
editorRef.value.focus();
- handleInput(); // Use the global handleInput without passing event args
+ handleInput();
};
- if (typeof fileOrSrc === 'string') { // It's a URL
+ if (typeof fileOrSrc === 'string') {
const tempImageForSize = new Image();
tempImageForSize.onload = () => {
setupAndInsert(fileOrSrc, tempImageForSize.naturalWidth, tempImageForSize.naturalHeight);
};
tempImageForSize.onerror = () => {
console.warn('Failed to load image from URL for size calculation:', fileOrSrc);
- setupAndInsert(fileOrSrc); // Insert even if size calculation fails
+ setupAndInsert(fileOrSrc);
};
tempImageForSize.src = fileOrSrc;
- } else if (fileOrSrc instanceof File && fileOrSrc.type.startsWith('image/')) { // It's a File object
+ } else if (fileOrSrc instanceof File && fileOrSrc.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const dataUrl = e.target.result;
@@ -869,7 +794,7 @@ const insertImage = (fileOrSrc, isUploaded = false, uploadedUrl = '') => {
};
tempImageForSize.onerror = () => {
console.warn('Failed to load image from FileReader for size calculation.');
- setupAndInsert(dataUrl); // Insert even if size calculation fails
+ setupAndInsert(dataUrl);
};
tempImageForSize.src = dataUrl;
};
@@ -904,24 +829,18 @@ const onUploadSendImg = async (event) => {
console.warn('Invalid file type for image upload:', file.type);
continue;
}
-
- // Optimistically insert a local preview using the already optimized insertImage function
- insertImage(file, false); // isUploaded = false, uploadedUrl = ''
+ insertImage(file, false);
const formData = new FormData();
formData.append('file', file);
- formData.append('source', 'fonchain-chat'); // Consider making 'source' configurable
+ formData.append('source', 'fonchain-chat');
try {
const res = await uploadImg(formData);
if (res && res.status === 0 && res.data && res.data.ori_url) {
- // Successfully uploaded. Update the preview image with the server URL.
- // Find the corresponding preview image. This is a simplified approach.
- // A more robust method would involve unique IDs for each preview.
const previewImages = editorRef.value.querySelectorAll('img[data-status="local-preview"][src^="data:image"]:not([data-uploaded-url])');
let replacedPreview = false;
if (previewImages.length > 0) {
- // Try to find the correct preview. Assuming the last one is the most recent.
const lastPreviewImage = previewImages[previewImages.length - 1];
if (lastPreviewImage && lastPreviewImage.src.startsWith('data:image')) {
lastPreviewImage.src = res.data.ori_url;
@@ -930,20 +849,14 @@ const onUploadSendImg = async (event) => {
if (res.data.width) lastPreviewImage.setAttribute('data-original-width', res.data.width);
if (res.data.height) lastPreviewImage.setAttribute('data-original-height', res.data.height);
replacedPreview = true;
- handleInput(); // Update editor state after modifying the image
+ handleInput();
}
}
if (!replacedPreview) {
- // If preview wasn't found/replaced, insert the uploaded image anew.
insertImage(res.data.ori_url, true, res.data.ori_url);
}
- // Emit an event that an image has been uploaded and inserted/updated
- // This event is for the parent component, if it needs to react to the final image URL.
- // The original emitCall('image_event', data) might be for sending the message immediately.
- // Clarify if this function should *send* the image or just *insert* it for later sending.
- // For now, let's assume the original intent was to emit an event that could lead to sending.
emit('editor-event', emitCall('image_event', {
url: res.data.ori_url,
width: res.data.width || 0,
@@ -953,7 +866,6 @@ const onUploadSendImg = async (event) => {
} else {
console.error('Image upload failed or received invalid response:', res);
- // Mark preview as failed
const previewImages = editorRef.value.querySelectorAll('img[data-status="local-preview"][src^="data:image"]:not([data-uploaded-url])');
if (previewImages.length > 0) {
const lastPreviewImage = previewImages[previewImages.length -1];
@@ -975,13 +887,12 @@ const onUploadSendImg = async (event) => {
}
}
}
- if (event.target) event.target.value = ''; // Reset file input
+ if (event.target) event.target.value = '';
};
async function onUploadFile(e) {
if (!e.target || !e.target.files || e.target.files.length === 0) return;
const file = e.target.files[0];
- // It's good practice to reset the input value immediately to allow re-selecting the same file
e.target.value = null;
const fileType = file.type;
@@ -989,42 +900,16 @@ async function onUploadFile(e) {
if (fileType.startsWith('image/')) {
eventName = 'image_event';
- // For images, we might want to use onUploadSendImg to handle preview and upload directly
- // Or, if this function is meant to be generic and just emit, then this is fine.
- // However, onUploadSendImg seems more specialized for editor image insertion.
- // Let's assume this onUploadFile is for a generic file picker that then emits.
- // If direct insertion is needed, call appropriate insert function or onUploadSendImg.
- // For consistency, if an image is chosen via this generic picker, and we want it in editor,
- // we should probably call onUploadSendImg or insertImage.
- // For now, sticking to emitting the raw file for parent to handle.
+
emit('editor-event', emitCall(eventName, file));
} else if (fileType.startsWith('video/')) {
eventName = 'video_event';
emit('editor-event', emitCall(eventName, file));
} else {
eventName = 'file_event';
- // If we want to insert a representation of the file into the editor before sending:
- // 1. Upload the file
- // 2. On success, insert a file node using a dedicated `insertFileNode` function.
- // For now, just emitting the raw file.
+
emit('editor-event', emitCall(eventName, file));
- // Example of how one might handle direct insertion after upload:
- /*
- const formData = new FormData();
- formData.append('file', file);
- formData.append('source', 'fonchain-chat');
- try {
- // Assuming a generic 'uploadActualFile' service exists
- const res = await uploadActualFile(formData);
- if (res && res.status === 0 && res.data && res.data.url) {
- insertFileNode(res.data.url, file.name, file.size); // New function needed
- } else {
- console.error('File upload failed:', res);
- }
- } catch (error) {
- console.error('Error uploading file:', error);
- }
- */
+
}
}
@@ -1073,30 +958,29 @@ const insertTextEmoji = (emojiText) => {
if (selection && selection.rangeCount > 0) {
range = selection.getRangeAt(0);
if (!editor.contains(range.commonAncestorContainer)) {
- // Range is outside the editor, reset to end of editor
+
range = document.createRange();
range.selectNodeContents(editor);
range.collapse(false);
}
} else {
- // No selection, create range at the end of the editor
+
range = document.createRange();
range.selectNodeContents(editor);
range.collapse(false);
}
- range.deleteContents(); // Clear any selected text or prepare cursor position
+ range.deleteContents();
const textNode = document.createTextNode(emojiText);
range.insertNode(textNode);
- // Move cursor after the inserted text node
range.setStartAfter(textNode);
range.collapse(true);
- selection.removeAllRanges(); // Deselect previous range
- selection.addRange(range); // Apply new range
+ selection.removeAllRanges();
+ selection.addRange(range);
- handleInput(); // Update editor state
+ handleInput();
};
@@ -1114,35 +998,29 @@ const insertImageEmoji = (imgSrc, altText) => {
if (!editor.contains(range.commonAncestorContainer)) {
range = document.createRange();
range.selectNodeContents(editor);
- range.collapse(false); // Move to the end
+ range.collapse(false);
}
} else {
range = document.createRange();
range.selectNodeContents(editor);
- range.collapse(false); // Move to the end
+ range.collapse(false);
}
- range.deleteContents(); // Clear any selected text or prepare cursor position
+ range.deleteContents();
const img = document.createElement('img');
img.src = imgSrc;
- img.alt = altText || 'emoji'; // Provide a default alt text
- img.className = 'editor-emoji'; // Class for styling
- img.setAttribute('data-role', 'emoji'); // For easier identification
- // Consider setting a standard size for emoji images via CSS or attributes
- // img.style.width = '20px';
- // img.style.height = '20px';
- // img.style.verticalAlign = 'middle';
+ img.alt = altText || 'emoji';
+ img.className = 'editor-emoji';
+ img.setAttribute('data-role', 'emoji');
range.insertNode(img);
- // Insert a space after the emoji for better typing experience
- const spaceNode = document.createTextNode('\u00A0'); // Non-breaking space, or use ' '
+ const spaceNode = document.createTextNode('\u00A0');
range.setStartAfter(img);
range.collapse(true);
range.insertNode(spaceNode);
- // Move cursor after the space
range.setStartAfter(spaceNode);
range.collapse(true);
@@ -1151,7 +1029,7 @@ const insertImageEmoji = (imgSrc, altText) => {
selection.addRange(range);
}
- handleInput(); // Update editor state
+ handleInput();
};
@@ -1160,7 +1038,7 @@ const onSubscribeMention = async (data) => {
const editorNode = editorRef.value;
editorNode.focus();
- await nextTick(); // Ensure focus and DOM updates are processed
+ await nextTick();
let selection = window.getSelection();
let range;
@@ -1168,29 +1046,27 @@ const onSubscribeMention = async (data) => {
if (selection && selection.rangeCount > 0) {
range = selection.getRangeAt(0);
if (!editorNode.contains(range.commonAncestorContainer)) {
- // If current selection is outside editor, move to the end of the editor
+
range = document.createRange();
range.selectNodeContents(editorNode);
- range.collapse(false); // false to collapse to the end
+ range.collapse(false);
}
} else {
- // No selection or invalid selection, create a new range at the end of the editor
+
range = document.createRange();
range.selectNodeContents(editorNode);
range.collapse(false);
}
- // Ensure selection is updated with the correct range
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
- insertMention(data, range); // Pass the live range to insertMention
+ insertMention(data, range);
} else {
- // Fallback if selection is null for some reason (should be rare)
const fallbackRange = document.createRange();
fallbackRange.selectNodeContents(editorNode);
fallbackRange.collapse(false);
- const newSelection = window.getSelection(); // Attempt to re-get selection
+ const newSelection = window.getSelection();
if (newSelection){
newSelection.removeAllRanges();
newSelection.addRange(fallbackRange);
@@ -1201,10 +1077,8 @@ const onSubscribeMention = async (data) => {
}
};
-// 在组件顶层作用域定义handleDeleteQuote函数
const handleDeleteQuote = function(e) {
- // 如果不是删除键或退格键,直接返回
- if (e.key !== 'Backspace' && e.key !== 'Delete') return;
+ if (e.key !== 'Backspace' && e.key !== 'Delete') return;
const selection = window.getSelection();
if (selection.rangeCount === 0) return;
@@ -1216,36 +1090,29 @@ const handleDeleteQuote = function(e) {
const quoteElement = editor.querySelector('.editor-quote');
if (!quoteElement) {
- // 如果没有引用元素,移除事件监听器
- editor.removeEventListener('keydown', handleDeleteQuote);
+ editor.removeEventListener('keydown', handleDeleteQuote);
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.startContainer === editor &&
quoteIndex === range.startOffset;
- // 检查是否在引用元素后按下删除键
- const isAfterQuote = e.key === 'Delete' &&
+ const isAfterQuote = e.key === 'Delete' &&
range.collapsed &&
range.startContainer === editor &&
quoteIndex === range.startOffset - 1;
if (isBeforeQuote || isAfterQuote) {
- // 阻止默认行为
- e.preventDefault();
+ e.preventDefault();
- // 移除引用元素
- quoteElement.remove();
+ quoteElement.remove();
quoteData.value = null;
- // 触发输入事件更新编辑器内容
- handleInput({ target: editor });
+ handleInput({ target: editor });
}
};
@@ -1254,10 +1121,7 @@ const onSubscribeQuote = (data) => {
quoteData.value = data;
const editor = editorRef.value;
- // Remove existing quotes
editor.querySelectorAll('.editor-quote').forEach(quote => quote.remove());
-
- // Save current selection if it's within the editor
const selection = window.getSelection();
let savedRange = null;
if (selection && selection.rangeCount > 0) {
@@ -1267,7 +1131,6 @@ const onSubscribeQuote = (data) => {
}
}
- // Create quote element safely
const quoteElement = document.createElement('div');
quoteElement.className = 'editor-quote';
quoteElement.contentEditable = 'false';
@@ -1298,15 +1161,12 @@ const onSubscribeQuote = (data) => {
closeButton.className = 'quote-close';
closeButton.textContent = '×';
quoteElement.appendChild(closeButton);
-
- // Insert quote at the beginning
if (editor.firstChild) {
editor.insertBefore(quoteElement, editor.firstChild);
} else {
editor.appendChild(quoteElement);
}
- // Ensure there's a node (like a zero-width space) after the quote for cursor placement
let nodeToPlaceCursorAfter = quoteElement;
const zeroWidthSpace = document.createTextNode('\u200B');
if (editor.lastChild === quoteElement || !quoteElement.nextSibling) {
@@ -1325,11 +1185,10 @@ const onSubscribeQuote = (data) => {
nodeToPlaceCursorAfter.remove();
}
quoteData.value = null;
- editor.removeEventListener('keydown', handleDeleteQuote); // Clean up listener
- handleInput(); // Update editor state
+ editor.removeEventListener('keydown', handleDeleteQuote); handleInput();
editor.focus();
} else {
- // Click on quote content, move cursor after the quote (or after the zeroWidthSpace)
+
const newRange = document.createRange();
newRange.setStartAfter(nodeToPlaceCursorAfter.parentNode === editor ? nodeToPlaceCursorAfter : quoteElement);
newRange.collapse(true);
@@ -1342,43 +1201,35 @@ const onSubscribeQuote = (data) => {
};
quoteElement.addEventListener('click', handleQuoteClick);
- editor.addEventListener('keydown', handleDeleteQuote); // Add keydown listener for deletion
+ editor.addEventListener('keydown', handleDeleteQuote);
- // Set timeout to allow DOM to update, then focus and set cursor
- setTimeout(() => {
+ setTimeout(() => {
editor.focus();
const newSelection = window.getSelection();
if (!newSelection) return;
let cursorPlaced = false;
- // Try to restore saved range if it's still valid
- if (savedRange) {
+ if (savedRange) {
try {
- // Check if the container of the saved range is still part of the editor
- if (editor.contains(savedRange.commonAncestorContainer) && savedRange.startContainer) {
+ if (editor.contains(savedRange.commonAncestorContainer) && savedRange.startContainer) {
newSelection.removeAllRanges();
newSelection.addRange(savedRange);
cursorPlaced = true;
}
} catch (err) {
- // If restoring fails, fallback to placing cursor after quote
- }
+ }
}
if (!cursorPlaced) {
const newRange = document.createRange();
- // Ensure nodeToPlaceCursorAfter is still valid and in the DOM
- if (nodeToPlaceCursorAfter && nodeToPlaceCursorAfter.parentNode === editor) {
+ if (nodeToPlaceCursorAfter && nodeToPlaceCursorAfter.parentNode === editor) {
newRange.setStartAfter(nodeToPlaceCursorAfter);
} else if (quoteElement.parentNode === editor && quoteElement.nextSibling) {
- // Fallback to after quote element's direct next sibling if zeroWidthSpace was removed or invalid
- newRange.setStartAfter(quoteElement.nextSibling);
+ newRange.setStartAfter(quoteElement.nextSibling);
} else if (quoteElement.parentNode === editor) {
- // Fallback to after quote element itself if it's the last child
- newRange.setStartAfter(quoteElement);
+ newRange.setStartAfter(quoteElement);
} else {
- // Ultimate fallback: end of editor
- newRange.selectNodeContents(editor);
+ newRange.selectNodeContents(editor);
newRange.collapse(false);
}
newRange.collapse(true);
@@ -1386,10 +1237,7 @@ const onSubscribeQuote = (data) => {
newSelection.addRange(newRange);
}
- editor.scrollTop = editor.scrollHeight; // Scroll to bottom if needed
- handleInput(); // Update editor state
- }, 0); // A small delay like 0 or 50ms is usually enough
-};
+ editor.scrollTop = editor.scrollHeight; handleInput(); }, 0); };
const onSubscribeEdit = (data) => {
editingMessage.value = data
diff --git a/src/views/office/index.vue b/src/views/office/index.vue
index 9edc0a5..aa5bfa3 100644
--- a/src/views/office/index.vue
+++ b/src/views/office/index.vue
@@ -57,7 +57,6 @@ const config = {
},
documentType,
editorConfig: {
-
mode: 'view',
lang: 'zh-CN',
user: {