海信医疗-远程超声管理平台-信创国产化
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1322 lines
37 KiB

1 month ago
<template>
<div class="message-editor">
<!-- Quill 编辑区 + 右下角发送按钮 容器 -->
<div class="quill-wrap">
<div ref="quillEditor" class="quill-container"></div>
<!-- 右下角发送区域 -->
<!-- 工具栏 -->
<div class="editor-toolbar">
<div class="editor-toolbar-left">
<el-button
type="text"
icon="el-icon-folder-opened"
@click="handleFileClick"
title="文件"
/>
<el-button
type="text"
icon="el-icon-scissors"
@click="handleScreenshotClick"
title="截屏"
/>
<el-dropdown @command="handleFontSizeChange">
<el-button type="text" style="font-size: 18px">A</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="10">10</el-dropdown-item>
<el-dropdown-item command="11">11</el-dropdown-item>
<el-dropdown-item command="12">12</el-dropdown-item>
<el-dropdown-item command="13">13</el-dropdown-item>
<el-dropdown-item command="14">14</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-dropdown
ref="atDropdown"
@command="handleAtSelect"
v-if="currentChat.scene == 4 && groupInfo"
>
<el-button type="text" style="font-size: 16px">@</el-button>
<el-dropdown-menu
slot="dropdown"
style="height: 200px; overflow-y: auto"
>
<el-dropdown-item
:command="{ id: 0, name: 'all' }"
style="
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 8px;
padding-bottom: 8px;
"
>
<div style="display: flex; align-items: center; gap: 8px">
<el-avatar :size="38" icon="el-icon-user-solid" />
所有人
</div>
</el-dropdown-item>
<el-dropdown-item
:command="item"
v-for="item in groupInfo.user_list"
:key="item.id"
style="
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 8px;
padding-bottom: 8px;
"
>
<div style="display: flex; align-items: center; gap: 8px">
<el-avatar
:size="38"
:src="
$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS +
item.avatar
"
/>
{{ item.name }}
</div>
<div style="color: #8492a6">
{{ item.role_name }}
</div>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<div class="editor-toolbar-right">
<el-button
type="text"
icon="el-icon-video-camera"
@click="handleTeachingClick(1)"
title="发起教学"
v-if="currentChat.scene == ContactsScene.GROUP"
/>
<el-button
type="text"
icon="el-icon-video-camera-solid"
@click="handleTeachingClick(9)"
title="发起会诊"
v-if="currentChat.scene == ContactsScene.PRIVATE"
/>
<el-button
type="primary"
size="mini"
:loading="sending"
@click="handleSendText"
>
发送
</el-button>
<el-dropdown @command="handleSendModeChange">
<el-button type="text">
{{ sendModeLabel }}<i class="el-icon-arrow-down el-icon--right" />
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="enter">Enter发送</el-dropdown-item>
<el-dropdown-item command="ctrlEnter">
Ctrl+Enter发送
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
1 month ago
</div>
</div>
<!-- 隐藏文件上传 -->
1 month ago
<input
ref="fileInput"
type="file"
style="display: none"
accept="image/*,video/*,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.zip,.rar,.txt"
@change="(e) => handleFileChange(e)"
1 month ago
/>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import Quill from "quill";
import "quill/dist/quill.core.css";
import "quill/dist/quill.snow.css";
1 month ago
import { sendPrivateMessage, sendMultiChatMessage } from "@/api/message";
import { uploadFile } from "@/utils/requestMinio";
import { takeScreenshot } from "@/utils/screenshot";
1 month ago
import {
MessageType,
ContactsScene,
MinioPaths,
MqttTopics,
} from "@/utils/constants";
import { meetingModes } from "@/api/videoCommunication";
1 month ago
export default {
name: "MessageEditor",
props: {
groupInfo: Object,
},
1 month ago
data() {
return {
MessageType,
ContactsScene,
quill: null,
currentRange: null,
1 month ago
sending: false,
maxFileSize: 100 * 1024 * 1024,
sendMode: this.getSystemSendMode(),
fontSizeMap: {
10: "small",
11: "small",
12: false,
13: "large",
14: "large",
},
// 记录被@的用户
1 month ago
selectedAtUsers: [],
meetingModes: meetingModes(),
// Quill初始化重试次数
quillInitRetryCount: 0,
// 存储编辑器中的文件信息
editorFiles: [],
maxQuillInitRetries: 20,
1 month ago
};
},
computed: {
...mapGetters(["currentChat", "userInfo", "config"]),
1 month ago
canSend() {
const text = this.quill ? this.quill.getText().trim() : "";
return text.length > 0 && this.currentChat && !this.sending;
1 month ago
},
sendModeLabel() {
return this.sendMode === "enter" ? "Enter发送" : "Ctrl+Enter发送";
1 month ago
},
},
mounted() {
this.$nextTick(() => {
this.initQuill();
});
document.addEventListener("keydown", this.handleGlobalKeydown);
window.addEventListener("systemSettingsChanged", this.handleSettingsChanged);
this.loadSendModeFromSettings();
1 month ago
},
beforeDestroy() {
if (this.quill) {
this.quill = null;
}
document.removeEventListener("keydown", this.handleGlobalKeydown);
window.removeEventListener("systemSettingsChanged", this.handleSettingsChanged);
},
watch: {
sendMode(newMode) {
this.saveSendModeToSettings(newMode);
},
1 month ago
},
methods: {
getSystemSendMode() {
try {
const settings = localStorage.getItem("systemSettings");
if (settings) {
const parsed = JSON.parse(settings);
if (parsed.basicForm && parsed.basicForm.sendKey) {
const savedKey = parsed.basicForm.sendKey;
if (savedKey === "Ctrl+Enter") {
return "ctrlEnter";
} else if (savedKey === "Enter") {
return "enter";
}
}
}
} catch (e) {
console.error("读取系统设置失败:", e);
}
return "ctrlEnter";
},
loadSendModeFromSettings() {
const mode = this.getSystemSendMode();
if (mode !== this.sendMode) {
this.sendMode = mode;
this.refreshQuillBindings();
}
},
saveSendModeToSettings(mode) {
try {
const settings = localStorage.getItem("systemSettings");
const parsed = settings ? JSON.parse(settings) : {};
if (!parsed.basicForm) {
parsed.basicForm = {};
}
parsed.basicForm.sendKey = mode === "ctrlEnter" ? "ctrl" : "enter";
localStorage.setItem("systemSettings", JSON.stringify(parsed));
} catch (e) {
console.error("保存发送模式失败:", e);
}
},
handleSettingsChanged(e) {
if (e.detail && e.detail.settings) {
this.loadSendModeFromSettings();
}
},
// 初始化Quill
initQuill() {
const editorDom = this.$refs.quillEditor;
if (!editorDom) {
this.quillInitRetryCount++;
if (this.quillInitRetryCount <= this.maxQuillInitRetries) {
console.warn(
`Quill container not found, retrying... (${this.quillInitRetryCount}/${this.maxQuillInitRetries})`
);
setTimeout(() => {
this.initQuill();
}, 100);
} else {
console.error("Quill container not found after maximum retries");
}
return;
}
this.quillInitRetryCount = 0;
if (this.quill) return;
// 初始化时注入键盘绑定
this.quill = new Quill(editorDom, {
theme: "snow",
placeholder: "请输入消息...",
modules: {
toolbar: false,
keyboard: {
bindings: this.getQuillKeyBindings(), // 抽离绑定配置,支持动态更新
},
},
readOnly: false,
});
const toolbar = this.quill.getModule("toolbar");
if (toolbar.container) {
toolbar.container.style.display = "none";
1 month ago
}
this.quill.on("selection-change", (range) => {
this.currentRange = range;
});
this.quill.on("text-change", (delta, oldDelta, source) => {
if (source === "user") {
this.handleTextChange(delta);
}
});
this.quill.root.addEventListener("paste", this.handlePaste, true);
// ========= 删除原来这里的 keydown 监听,改用 bindings 控制 =========
},
// 根据当前 sendMode 返回 Quill 按键规则
getQuillKeyBindings() {
const self = this;
if (this.sendMode === "enter") {
// Enter发送模式:Enter=发送,Ctrl/Cmd+Enter=换行,Shift+Enter=换行
return {
enter: {
key: "Enter",
handler() {
self.handleSendText();
},
},
ctrlEnter: {
key: "Enter",
ctrlKey: true,
metaKey: true,
handler(range) {
const quill = self.quill;
quill.insertText(range.index, "\n");
quill.setSelection(range.index + 1);
},
},
shiftEnter: {
key: "Enter",
shiftKey: true,
handler(range) {
const quill = self.quill;
quill.insertText(range.index, "\n");
quill.setSelection(range.index + 1);
},
},
};
} else {
// Ctrl+Enter发送模式:Enter=换行,Shift+Enter=换行,Ctrl/Cmd+Enter=发送
return {
enter: {
key: "Enter",
shiftKey: null,
handler(range) {
const quill = self.quill;
quill.insertText(range.index, "\n");
quill.setSelection(range.index + 1);
},
},
shiftEnter: {
key: "Enter",
shiftKey: true,
handler(range) {
const quill = self.quill;
quill.insertText(range.index, "\n");
quill.setSelection(range.index + 1);
},
},
ctrlEnter: {
key: "Enter",
ctrlKey: true,
metaKey: true,
handler() {
self.handleSendText();
},
},
};
}
},
// 切换发送模式后,刷新Quill键盘绑定
refreshQuillBindings() {
if (!this.quill) return;
const keyboard = this.quill.getModule("keyboard");
// 清空旧绑定,替换新规则
keyboard.bindings = this.getQuillKeyBindings();
1 month ago
},
handleGlobalKeydown(e) {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
const tag = document.activeElement.tagName;
if (["INPUT", "TEXTAREA", "SELECT"].includes(tag)) return;
const text = this.quill ? this.quill.getText().trim() : "";
if (!text || this.sending) return;
// 仅在 ctrlEnter 模式下全局快捷键生效
if (this.sendMode !== "ctrlEnter") return;
e.preventDefault();
this.handleSendText();
}
},
// 处理文本变化,检测@符号输入
handleTextChange(delta) {
try {
if (
!this.currentChat ||
this.currentChat.scene !== ContactsScene.GROUP
) {
return;
}
if (!this.groupInfo) return;
if (delta.ops) {
delta.ops.forEach((op) => {
if (op.insert && typeof op.insert === "string") {
const atIndex = op.insert.lastIndexOf("@");
if (atIndex !== -1 && atIndex === op.insert.length - 1) {
this.showAtDropdown();
}
}
});
}
} catch (e) {
console.warn("handleTextChange error:", e);
}
},
// 显示@成员下拉框
showAtDropdown() {
try {
const dropdownEl = this.$refs.atDropdown;
if (dropdownEl && dropdownEl.$el) {
dropdownEl.$el.click();
}
} catch (e) {
console.warn("showAtDropdown error:", e);
}
},
1 month ago
clearEditor() {
if (this.quill) {
this.quill.setText("");
}
this.editorFiles = [];
},
// 字号切换 - 调整消息显示区域的文字大小
handleFontSizeChange(size) {
this.$emit("font-size-change", parseInt(size));
},
// 发送方式切换
handleSendModeChange(mode) {
this.sendMode = mode;
// 切换后刷新键盘规则立即生效
this.refreshQuillBindings();
},
// 文件按钮
handleFileClick() {
this.$refs.fileInput.click();
},
// 截屏
async handleScreenshotClick() {
try {
await takeScreenshot(
async (blob) => {
try {
const fileName = `screenshot_${Date.now()}.png`;
const objectName = this.getObjectPath(fileName);
await this.ensureMinioInitialized();
const config = this.$store.getters.config;
const bucket = config.MINIO_BUCKET_FILE || "remote-file-test";
const result = await uploadFile(
bucket,
objectName,
blob,
{},
(percent) => {}
);
const uploadedPath = bucket + "/" + result.objectName;
// 创建文件对象用于 insertFileToEditor
const screenshotFile = new File([blob], fileName, {
type: "image/png",
});
await this.insertFileToEditor(screenshotFile, uploadedPath);
this.$message.success("截图已插入到编辑器");
} catch (e) {
this.$message.error("截图上传失败: " + e.message);
}
},
() => {
this.$message.info("已取消截图");
}
);
} catch (e) {
this.$message.error("截图失败: " + e.message);
}
},
// ========== 核心:选中@成员 插入富文本 ==========
handleAtSelect(user) {
if (!this.quill || !user) return;
const index = this.currentRange
? this.currentRange.index
: this.quill.getLength();
const insertText = `@${user.name}`;
this.quill.insertText(index, insertText);
this.quill.formatText(index, insertText.length, {
color: "#409eff",
fontWeight: "500",
});
this.quill.setSelection(index + insertText.length);
const hasUser = this.selectedAtUsers.find((u) => u.id === user.id);
if (!hasUser) {
this.selectedAtUsers.push(user);
1 month ago
}
},
handleTeachingClick(meetingType) {
const targetMode = this.meetingModes.find(
(item) => item.type === meetingType
);
console.log(targetMode);
// 正常跳转
this.$router.push({
name: targetMode.routeName,
query: {
name: this.userInfo.group,
roomId_id: this.generateRoomId(targetMode.type, this.userInfo.id),
},
});
1 month ago
},
generateRoomId(meetingType, id) {
// 1. id固定3位,不足左侧补0
const idStr = String(id).padStart(3, "0");
// 拼接 = 会议类型 + 3位id + 前缀
const roomId = meetingType + idStr + this.config.MEETING_PREFIX;
return roomId;
},
// 提取@用户ID(发送接口使用)
extractAtUsers(html) {
const ids = [];
this.selectedAtUsers.forEach((user) => {
if (html.includes(`@${user.name}`)) {
ids.push(user.id);
}
});
return ids;
},
// 发送消息
1 month ago
async handleSendText() {
if (!this.quill || !this.currentChat) return;
const html = this.quill.root.innerHTML;
const textContent = this.quill.getText().trim();
// 检查是否有内容可以发送(文本或文件)
const hasText = textContent.replace(/\s+/g, "").length > 0;
const files = this.extractFilesFromEditor();
if (!hasText && files.length === 0) {
this.$message.warning("请输入内容或上传文件");
return;
}
1 month ago
this.sending = true;
try {
const messages = this.parseEditorContent(html, textContent);
for (let i = 0; i < messages.length; i++) {
const messageData = messages[i];
const messageId = this.generateMessageId();
this.$emit("send-message", {
...messageData,
is_self: true,
sending: true,
message_id: messageId,
});
const body = {
client_id: this.getClientId(),
message: messageData,
target_id: this.currentChat.id,
topic: this.getTopic(),
};
if (this.currentChat.scene === ContactsScene.PRIVATE) {
await sendPrivateMessage(body);
} else if (this.currentChat.scene === ContactsScene.GROUP) {
await sendMultiChatMessage(body);
}
this.$emit("update-message-status", messageId, { sending: false });
}
1 month ago
this.clearEditor();
this.selectedAtUsers = [];
} catch (e) {
this.$message.error("发送失败: " + (e.message || "未知错误"));
} finally {
this.sending = false;
}
},
// 解析编辑器内容,分离文本和文件
parseEditorContent(html, textContent) {
const messages = [];
const atUserIds = this.extractAtUsers(html);
const textOnly = textContent.replace(/\s+/g, "").length > 0;
const files = this.extractFilesFromEditor();
if (textOnly) {
messages.push({
at_users: atUserIds,
message_id: 0,
payload: {
content: textContent,
file_duration: 0,
file_ico: "",
file_name: "",
file_path: "",
file_size: 0,
file_type: "",
},
1 month ago
scene: this.currentChat.scene,
source_id: this.userInfo.id,
target_id: this.currentChat.id,
timestamp: 0,
type: "text",
});
}
1 month ago
files.forEach((file) => {
const msgType = this.getMessageType(file.type);
messages.push({
at_users: [],
message_id: 0,
payload: {
content: "",
file_duration: 0,
file_ico: file.icon,
file_name: file.name,
file_path: file.path,
file_size: file.size || 0,
file_type: file.type,
},
scene: this.currentChat.scene,
source_id: this.userInfo.id,
1 month ago
target_id: this.currentChat.id,
timestamp: 0,
type: msgType,
1 month ago
});
});
1 month ago
return messages;
},
1 month ago
// 从编辑器中提取所有文件信息
extractFilesFromEditor() {
const files = [];
if (!this.quill) return files;
const config = this.$store.getters.config;
const endpoint = config.MINIO_ENDPOINT_HTTPS || "";
this.quill.root.querySelectorAll("img").forEach((img) => {
const src = img.src;
if (src.startsWith(endpoint)) {
const filePath = src.replace(endpoint, "");
const fileName = filePath.split("/").pop();
files.push({
name: fileName,
path: filePath,
type: "image",
size: 0,
icon: "",
});
1 month ago
}
});
1 month ago
this.quill.root.querySelectorAll(".file-card").forEach((card) => {
const fileIndex = card.getAttribute("data-file-index");
if (fileIndex !== null && this.editorFiles[fileIndex]) {
files.push(this.editorFiles[fileIndex]);
}
});
return files;
},
// 根据文件类型获取消息类型
getMessageType(fileType) {
switch (fileType) {
case "image":
return "image";
case "video":
return "video";
case "audio":
return "audio";
default:
return "file";
}
1 month ago
},
// 文件相关逻辑
async handleFileChange(e) {
1 month ago
const files = e.target.files;
if (!files || !files.length) return;
const file = files[0];
e.target.value = "";
if (file.size > this.maxFileSize) {
this.$message.warning("文件大小不能超过100MB");
return;
}
if (!this.currentChat) {
this.$message.warning("请先选择聊天对象");
return;
}
try {
await this.ensureMinioInitialized();
const fileName = file.name;
const objectName = this.getObjectPath(fileName);
const config = this.$store.getters.config;
const bucket = config.MINIO_BUCKET_FILE || "remote-file-test";
const result = await uploadFile(
bucket,
objectName,
file,
{},
(percent) => {}
);
const uploadedPath = bucket + "/" + result.objectName;
// 所有文件都插入到编辑器
await this.insertFileToEditor(file, uploadedPath);
this.$message.success("文件上传成功");
} catch (e) {
this.$message.error("文件上传失败: " + e.message);
}
},
// 直接发送文件消息
async sendFileMessageDirect(file, filePath) {
if (!this.currentChat) return;
const messageId = this.generateMessageId();
const fileType = this.getFileType(file);
const messageType = file.type.startsWith("video/") ? "video" : "file";
const message = {
at_users: [],
message_id: messageId,
payload: {
content: "",
file_duration: 0,
file_ico: this.getFileIcon(fileType),
file_name: file.name,
file_path: filePath,
file_size: file.size,
file_type: fileType,
},
scene: this.currentChat.scene,
source_id: this.userInfo.id,
target_id: this.currentChat.id,
timestamp: Date.now(),
type: messageType,
is_self: true,
sending: true,
};
this.$emit("send-message", message);
try {
const body = {
client_id: this.getClientId(),
message: {
at_users: [],
message_id: 0,
payload: {
content: "",
file_duration: 0,
file_ico: this.getFileIcon(fileType),
file_name: file.name,
file_path: filePath,
file_size: file.size,
file_type: fileType,
},
scene: this.currentChat.scene,
source_id: this.userInfo.id,
target_id: this.currentChat.id,
timestamp: 0,
type: messageType,
},
target_id: this.currentChat.id,
topic: this.getTopic(),
};
if (this.currentChat.scene === ContactsScene.PRIVATE) {
await sendPrivateMessage(body);
} else if (this.currentChat.scene === ContactsScene.GROUP) {
await sendMultiChatMessage(body);
}
this.$emit("update-message-status", messageId, { sending: false });
} catch (e) {
this.$message.error("发送文件失败: " + e.message);
this.$emit("update-message-status", messageId, {
sending: false,
sendFailed: true,
});
}
1 month ago
},
// 获取文件类型
getFileType(file) {
const ext = file.name.split(".").pop().toLowerCase();
const imageExts = ["jpg", "jpeg", "png", "gif", "bmp", "webp"];
const videoExts = ["mp4", "webm", "ogg", "mov", "avi"];
const docExts = ["doc", "docx"];
const excelExts = ["xls", "xlsx"];
const pptExts = ["ppt", "pptx"];
if (imageExts.includes(ext)) return "image";
if (videoExts.includes(ext)) return "video";
if (docExts.includes(ext)) return ext;
if (excelExts.includes(ext)) return ext;
if (pptExts.includes(ext)) return ext;
if (ext === "pdf") return "pdf";
if (ext === "zip" || ext === "rar") return ext;
if (ext === "txt") return "txt";
return "file";
},
1 month ago
async handlePaste(e) {
const items = e.clipboardData.items;
if (!items) return;
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.type.indexOf("image") !== -1) {
const file = item.getAsFile();
if (file) {
e.preventDefault();
await this.sendFileMessage(file, "image");
return;
}
}
}
},
async sendFileMessage(file, type) {
if (!this.currentChat) return;
const messageId = this.generateMessageId();
console.log("sendFileMessage", file);
const fileName = file.name;
1 month ago
const objectName = this.getObjectPath(fileName);
const fileType =
type === "image"
? "image"
: type === "video"
? "video"
: type === "audio"
? "audio"
: file.name.split(".").pop().toLowerCase() || "file";
const localPayload = {
content: "",
file_duration: 0,
file_ico: "",
1 month ago
file_name: file.name,
file_path: objectName,
1 month ago
file_size: file.size,
file_type: fileType,
1 month ago
};
const message = {
at_users: [],
message_id: 0,
payload: localPayload,
1 month ago
scene: this.currentChat.scene,
source_id: this.userInfo.id,
target_id: this.currentChat.id,
timestamp: 0,
type: type === "image" ? "image" : "file",
1 month ago
};
this.$emit("send-message", {
...message,
1 month ago
is_self: true,
sending: true,
isUploading: true,
message_id: messageId,
});
try {
await this.uploadToMinIO(file, objectName);
const body = {
client_id: this.getClientId(),
message: {
at_users: [],
message_id: 0,
payload: {
content: "",
file_duration: 0,
file_ico: type === "image" ? "" : this.getFileIcon(fileType),
file_name: file.name,
file_path: objectName,
file_size: file.size,
file_type: fileType,
},
scene: this.currentChat.scene,
source_id: this.userInfo.id,
target_id: this.currentChat.id,
timestamp: 0,
type: type === "image" ? "image" : "file",
},
1 month ago
target_id: this.currentChat.id,
topic: this.getTopic(),
1 month ago
};
if (this.currentChat.scene === ContactsScene.PRIVATE) {
await sendPrivateMessage(body);
} else if (this.currentChat.scene === ContactsScene.GROUP) {
await sendMultiChatMessage(body);
}
this.$emit("update-message-status", messageId, {
sending: false,
isUploading: false,
payload: type === "image" ? objectName : serverPayload,
});
if (type === "image" && this.quill) {
const index = this.currentRange
? this.currentRange.index
: this.quill.getLength();
this.quill.insertEmbed(index, "image", objectName);
this.quill.setSelection(index + 1);
}
1 month ago
} catch (e) {
this.$message.error("发送文件失败: " + e.message);
this.$emit("update-message-status", messageId, {
sending: false,
sendFailed: true,
isUploading: false,
});
}
},
insertImageToEditor(imagePath) {
if (!this.quill) return;
const index = this.currentRange
? this.currentRange.index
: this.quill.getLength();
const imageUrl =
this.$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS + imagePath;
console.log(imageUrl);
this.quill.insertEmbed(index, "image", imageUrl);
this.quill.setSelection(index + 1);
},
insertFileToEditor(file, filePath) {
if (!this.quill) return;
const index = this.currentRange
? this.currentRange.index
: this.quill.getLength();
const fileType = this.getFileType(file);
const fileInfo = {
name: file.name,
path: filePath,
type: fileType,
size: file.size,
icon: this.getFileIcon(fileType),
};
if (!this.editorFiles) {
this.editorFiles = [];
}
this.editorFiles.push(fileInfo);
if (fileType === "image") {
const imageUrl =
this.$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS + filePath;
this.quill.insertEmbed(index, "image", imageUrl);
} else {
const fileSize = this.formatFileSize(file.size);
const fileHtml = `<div class="file-card" data-file-index="${
this.editorFiles.length - 1
}">
<div class="file-icon-wrapper">
<img src="${fileInfo.icon}" class="file-icon" />
</div>
<div class="file-info">
<div class="file-name">${file.name}</div>
<div class="file-size">${fileSize}</div>
</div>
<div class="file-remove" onclick="this.parentElement.remove()">
<i class="el-icon-close"></i>
</div>
</div>`;
this.quill.clipboard.dangerouslyPasteHTML(index, fileHtml);
}
this.quill.setSelection(index + 1);
},
formatFileSize(bytes) {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
},
getFileType(file) {
const fileName = file.name.toLowerCase();
const ext = fileName.split(".").pop();
if (
file.type.startsWith("image/") ||
["jpg", "jpeg", "png", "gif", "bmp", "svg"].includes(ext)
) {
return "image";
}
if (
file.type.startsWith("video/") ||
["mp4", "webm", "ogg", "mov", "avi"].includes(ext)
) {
return "video";
}
if (
file.type.startsWith("audio/") ||
["mp3", "wav", "ogg"].includes(ext)
) {
return "audio";
}
if (["zip", "rar", "7z", "tar", "gz"].includes(ext)) {
return "zip";
}
if (["doc", "docx"].includes(ext)) {
return "doc";
}
if (["xls", "xlsx"].includes(ext)) {
return "xls";
}
if (["ppt", "pptx"].includes(ext)) {
return "ppt";
}
if (ext === "pdf") {
return "pdf";
}
if (ext === "txt") {
return "txt";
}
return "other";
},
ensureMinioInitialized() {
const config = this.$store.getters.config;
if (!config || !config.MINIO_ENDPOINT) {
return this.$store.dispatch("GetNetConfig");
}
return Promise.resolve();
},
1 month ago
async uploadToMinIO(file, objectName) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async () => {
try {
const formData = new FormData();
formData.append("file", file);
formData.append("object_name", objectName);
1 month ago
const { default: request } = await import("@/utils/request");
1 month ago
await request({
url: "/api/v1/common/upload",
method: "post",
data: formData,
headers: { "Content-Type": "multipart/form-data" },
});
resolve();
} catch (err) {
reject(err);
}
};
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
},
getMessageTypeByFileType(type, file) {
if (type === "image") return MessageType.IMAGE;
if (type === "video") return MessageType.VIDEO;
if (file.type.startsWith("audio/")) return MessageType.AUDIO;
return MessageType.FILE;
},
generateMessageId() {
return (
"msg_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9)
);
},
getObjectPath(fileName) {
let sceneDir;
switch (this.currentChat.scene) {
case ContactsScene.GROUP:
sceneDir = MinioPaths.FILE_MULTICHAT;
break;
case ContactsScene.NOTIFY:
sceneDir = MinioPaths.FILE_SYSTEM_NOTIFY;
break;
case ContactsScene.PRIVATE:
sceneDir = MinioPaths.FILE_PRIVATE;
break;
default:
sceneDir = MinioPaths.FILE_PRIVATE;
break;
}
const sourceId = this.userInfo.id;
const targetId = this.currentChat.id;
return `${sceneDir}${sourceId}-${targetId}/${fileName}`;
1 month ago
},
getTopic() {
if (this.currentChat.scene === ContactsScene.PRIVATE) {
return MqttTopics.PRIVATE_CHAT + this.userInfo.id;
}
return MqttTopics.MULTI_CHAT + this.currentChat.id;
},
getClientId() {
return MqttTopics.CLIENT_ID_PREFIX + this.userInfo?.id;
},
getFileIcon(fileType) {
const iconMap = {
doc: ":/message/image/Message/file_icon/doc.svg",
docx: ":/message/image/Message/file_icon/docx.svg",
xls: ":/message/image/Message/file_icon/xls.svg",
xlsx: ":/message/image/Message/file_icon/xlsx.svg",
ppt: ":/message/image/Message/file_icon/ppt.svg",
pptx: ":/message/image/Message/file_icon/pptx.svg",
pdf: ":/message/image/Message/file_icon/pdf.svg",
zip: ":/message/image/Message/file_icon/zip.svg",
rar: ":/message/image/Message/file_icon/rar.svg",
txt: ":/message/image/Message/file_icon/txt.svg",
video: ":/message/image/Message/file_icon/video.svg",
audio: ":/message/image/Message/file_icon/audio.svg",
};
return iconMap[fileType] || "";
1 month ago
},
},
};
</script>
<style lang="scss" scoped>
.message-editor {
background: #fff;
padding: 10px 16px;
.quill-wrap {
display: flex;
flex-direction: column;
border: 1px solid #e4e7ed;
border-radius: 4px;
gap: 8px;
.quill-container {
height: 100px;
min-height: 100px;
overflow-y: auto;
box-sizing: border-box;
::v-deep .ql-container {
height: 100%;
font-size: 14px;
}
1 month ago
::v-deep .ql-editor {
min-height: 100%;
padding: 8px;
img {
max-width: 100%;
height: auto;
display: block;
margin: 4px 0;
border-radius: 4px;
}
}
1 month ago
::v-deep .ql-toolbar {
display: none;
}
}
.editor-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
1 month ago
}
.editor-toolbar-left,
.editor-toolbar-right {
display: flex;
align-items: center;
gap: 12px;
1 month ago
.el-button {
margin: 0;
padding: 4px 8px;
}
1 month ago
}
}
}
.file-card {
display: inline-flex;
align-items: center;
background: #f5f7fa;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 8px 12px;
margin: 4px 4px 4px 0;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: #eef2f7;
border-color: #c0c4cc;
}
.file-icon-wrapper {
width: 32px;
height: 32px;
border-radius: 6px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
flex-shrink: 0;
.file-icon {
width: 18px;
height: 18px;
object-fit: contain;
filter: brightness(0) invert(1);
}
}
.file-info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
.file-name {
font-size: 13px;
color: #303133;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 180px;
}
.file-size {
font-size: 11px;
color: #909399;
}
}
.file-remove {
width: 20px;
height: 20px;
border-radius: 50%;
background: #ccc;
display: flex;
align-items: center;
justify-content: center;
margin-left: 10px;
cursor: pointer;
opacity: 0;
transition: all 0.2s ease;
i {
font-size: 12px;
color: #fff;
}
&:hover {
background: #f56c6c;
}
}
&:hover .file-remove {
opacity: 1;
}
}
</style>