|
|
|
|
<template>
|
|
|
|
|
<div class="message-editor">
|
|
|
|
|
<!-- 工具栏 -->
|
|
|
|
|
<div class="editor-toolbar">
|
|
|
|
|
<el-tooltip content="文件" placement="top">
|
|
|
|
|
<el-button
|
|
|
|
|
type="text"
|
|
|
|
|
icon="el-icon-folder-opened"
|
|
|
|
|
@click="handleFileClick"
|
|
|
|
|
/>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
|
|
|
|
|
<el-tooltip content="截屏" placement="top">
|
|
|
|
|
<el-button
|
|
|
|
|
type="text"
|
|
|
|
|
icon="el-icon-scissors"
|
|
|
|
|
@click="handleScreenshotClick"
|
|
|
|
|
/>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
|
|
|
|
|
<!-- 字号下拉 -->
|
|
|
|
|
<el-dropdown @command="handleFontSizeChange">
|
|
|
|
|
<el-button type="text"> A </el-button>
|
|
|
|
|
<el-dropdown-menu slot="dropdown">
|
|
|
|
|
<el-dropdown-item command="10">10px</el-dropdown-item>
|
|
|
|
|
<el-dropdown-item command="11">11px</el-dropdown-item>
|
|
|
|
|
<el-dropdown-item command="12">12px</el-dropdown-item>
|
|
|
|
|
<el-dropdown-item command="13">13px</el-dropdown-item>
|
|
|
|
|
<el-dropdown-item command="14">14px</el-dropdown-item>
|
|
|
|
|
</el-dropdown-menu>
|
|
|
|
|
</el-dropdown>
|
|
|
|
|
<!-- @成员下拉 仅群聊显示 -->
|
|
|
|
|
<el-dropdown @command="handleAtSelect" v-if="currentChat.scene == 4">
|
|
|
|
|
<el-button type="text"> @ </el-button>
|
|
|
|
|
<el-dropdown-menu
|
|
|
|
|
slot="dropdown"
|
|
|
|
|
style="height: 200px; overflow-y: auto"
|
|
|
|
|
>
|
|
|
|
|
<el-dropdown-item
|
|
|
|
|
command="{ id: '0', name: '所有人' }"
|
|
|
|
|
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>
|
|
|
|
|
|
|
|
|
|
<el-tooltip content="发起教学" placement="top">
|
|
|
|
|
<el-button
|
|
|
|
|
type="text"
|
|
|
|
|
icon="el-icon-video-camera"
|
|
|
|
|
@click="handleTeachingClick"
|
|
|
|
|
/>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
|
|
|
|
|
<el-tooltip content="发起会诊" placement="top">
|
|
|
|
|
<el-button
|
|
|
|
|
type="text"
|
|
|
|
|
icon="el-icon-video-camera-solid"
|
|
|
|
|
@click="handleConsultClick"
|
|
|
|
|
/>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Quill 编辑区 + 右下角发送按钮 容器 -->
|
|
|
|
|
<div class="quill-wrap">
|
|
|
|
|
<div ref="quillEditor" class="quill-container"></div>
|
|
|
|
|
<!-- 右下角发送区域 -->
|
|
|
|
|
<div class="send-area">
|
|
|
|
|
<el-button
|
|
|
|
|
type="primary"
|
|
|
|
|
size="small"
|
|
|
|
|
:disabled="!canSend"
|
|
|
|
|
:loading="sending"
|
|
|
|
|
@click="handleSendText"
|
|
|
|
|
>
|
|
|
|
|
发送
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-dropdown @command="handleSendModeChange">
|
|
|
|
|
<el-button type="text" size="small">
|
|
|
|
|
<span class="send-mode-text">{{ sendModeLabel }}</span>
|
|
|
|
|
<i class="el-icon-arrow-down el-icon--right"></i>
|
|
|
|
|
</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>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 隐藏文件上传 -->
|
|
|
|
|
<input
|
|
|
|
|
ref="fileInput"
|
|
|
|
|
type="file"
|
|
|
|
|
style="display: none"
|
|
|
|
|
@change="(e) => handleFileChange(e, 'file')"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
import { mapGetters } from "vuex";
|
|
|
|
|
import Quill from "quill";
|
|
|
|
|
import "quill/dist/quill.core.css";
|
|
|
|
|
import "quill/dist/quill.snow.css";
|
|
|
|
|
import { sendPrivateMessage, sendMultiChatMessage } from "@/api/message";
|
|
|
|
|
import {
|
|
|
|
|
MessageType,
|
|
|
|
|
ContactsScene,
|
|
|
|
|
MinioPaths,
|
|
|
|
|
MqttTopics,
|
|
|
|
|
} from "@/utils/constants";
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
name: "MessageEditor",
|
|
|
|
|
props: {
|
|
|
|
|
groupInfo: Object,
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
quill: null,
|
|
|
|
|
currentRange: null,
|
|
|
|
|
sending: false,
|
|
|
|
|
maxFileSize: 100 * 1024 * 1024,
|
|
|
|
|
sendMode: "enter",
|
|
|
|
|
fontSizeMap: {
|
|
|
|
|
10: "small",
|
|
|
|
|
11: "small",
|
|
|
|
|
12: false,
|
|
|
|
|
13: "large",
|
|
|
|
|
14: "large",
|
|
|
|
|
},
|
|
|
|
|
// 记录被@的用户
|
|
|
|
|
selectedAtUsers: [],
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
computed: {
|
|
|
|
|
...mapGetters(["currentChat", "userInfo"]),
|
|
|
|
|
|
|
|
|
|
canSend() {
|
|
|
|
|
const text = this.quill ? this.quill.getText().trim() : "";
|
|
|
|
|
return text.length > 0 && this.currentChat && !this.sending;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
sendModeLabel() {
|
|
|
|
|
return this.sendMode === "enter" ? "Enter发送" : "Ctrl+Enter发送";
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
mounted() {
|
|
|
|
|
this.initQuill();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
beforeDestroy() {
|
|
|
|
|
this.quill = null;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
methods: {
|
|
|
|
|
// 初始化Quill
|
|
|
|
|
initQuill() {
|
|
|
|
|
const editorDom = this.$refs.quillEditor;
|
|
|
|
|
this.quill = new Quill(editorDom, {
|
|
|
|
|
theme: "snow",
|
|
|
|
|
placeholder: "请输入消息...",
|
|
|
|
|
modules: {
|
|
|
|
|
toolbar: false,
|
|
|
|
|
},
|
|
|
|
|
readOnly: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const toolbar = this.quill.getModule("toolbar");
|
|
|
|
|
if (toolbar.container) {
|
|
|
|
|
toolbar.container.style.display = "none";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 监听光标位置
|
|
|
|
|
this.quill.on("selection-change", (range) => {
|
|
|
|
|
this.currentRange = range;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 粘贴图片
|
|
|
|
|
this.quill.root.addEventListener("paste", this.handlePaste, true);
|
|
|
|
|
|
|
|
|
|
// 回车发送逻辑
|
|
|
|
|
this.quill.root.addEventListener("keydown", (e) => {
|
|
|
|
|
if (this.sendMode === "enter") {
|
|
|
|
|
if (e.key === "Enter" && !e.ctrlKey && !e.shiftKey) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.handleSendText();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.handleSendText();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
clearEditor() {
|
|
|
|
|
if (this.quill) {
|
|
|
|
|
this.quill.setText("");
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 字号切换
|
|
|
|
|
handleFontSizeChange(size) {
|
|
|
|
|
const quillSize = this.fontSizeMap[size];
|
|
|
|
|
if (this.quill) {
|
|
|
|
|
this.quill.format("size", quillSize);
|
|
|
|
|
this.$refs.quillEditor.style.fontSize = size + "px";
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 发送方式切换
|
|
|
|
|
handleSendModeChange(mode) {
|
|
|
|
|
this.sendMode = mode;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 文件按钮
|
|
|
|
|
handleFileClick() {
|
|
|
|
|
this.$refs.fileInput.click();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 截屏
|
|
|
|
|
handleScreenshotClick() {
|
|
|
|
|
this.$emit("open-screenshot");
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// ========== 核心:选中@成员 插入富文本 ==========
|
|
|
|
|
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.setSelection(index + insertText.length);
|
|
|
|
|
// 记录被@用户(去重)
|
|
|
|
|
const hasUser = this.selectedAtUsers.find((u) => u.id === user.id);
|
|
|
|
|
if (!hasUser) {
|
|
|
|
|
this.selectedAtUsers.push(user);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleTeachingClick() {
|
|
|
|
|
this.$emit("open-teaching");
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleConsultClick() {
|
|
|
|
|
this.$emit("open-consult");
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 提取@用户ID(发送接口使用)
|
|
|
|
|
extractAtUsers(html) {
|
|
|
|
|
const ids = [];
|
|
|
|
|
this.selectedAtUsers.forEach((user) => {
|
|
|
|
|
if (html.includes(`@${user.name}`)) {
|
|
|
|
|
ids.push(user.id);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return ids;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 发送消息
|
|
|
|
|
async handleSendText() {
|
|
|
|
|
if (!this.quill || !this.currentChat) return;
|
|
|
|
|
const html = this.quill.root.innerHTML;
|
|
|
|
|
const text = this.quill.getText().trim();
|
|
|
|
|
if (!text) return;
|
|
|
|
|
|
|
|
|
|
this.sending = true;
|
|
|
|
|
const messageId = this.generateMessageId();
|
|
|
|
|
const timestamp = Date.now();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const atUserIds = this.extractAtUsers(html);
|
|
|
|
|
|
|
|
|
|
const message = {
|
|
|
|
|
at_users: atUserIds,
|
|
|
|
|
message_id: 0,
|
|
|
|
|
payload: {
|
|
|
|
|
content: text,
|
|
|
|
|
file_duration: 0,
|
|
|
|
|
file_ico: "",
|
|
|
|
|
file_name: "",
|
|
|
|
|
file_path: "",
|
|
|
|
|
file_size: 0,
|
|
|
|
|
file_type: "",
|
|
|
|
|
},
|
|
|
|
|
scene: this.currentChat.scene,
|
|
|
|
|
source_id: this.userInfo.id,
|
|
|
|
|
target_id: this.currentChat.id,
|
|
|
|
|
timestamp: 0,
|
|
|
|
|
type: "text",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const body = {
|
|
|
|
|
client_id: this.getClientId(),
|
|
|
|
|
message,
|
|
|
|
|
target_id: this.currentChat.id,
|
|
|
|
|
topic: this.getTopic(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.$emit("send-message", {
|
|
|
|
|
...message,
|
|
|
|
|
is_self: true,
|
|
|
|
|
sending: true,
|
|
|
|
|
message_id: messageId,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.clearEditor();
|
|
|
|
|
this.selectedAtUsers = [];
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
this.sending = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 文件相关逻辑
|
|
|
|
|
async handleFileChange(e, type) {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
await this.sendFileMessage(file, type);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
const fileName = this.generateFileName(file.name);
|
|
|
|
|
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: "",
|
|
|
|
|
file_name: file.name,
|
|
|
|
|
file_path: objectName,
|
|
|
|
|
file_size: file.size,
|
|
|
|
|
file_type: fileType,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const message = {
|
|
|
|
|
at_users: [],
|
|
|
|
|
message_id: 0,
|
|
|
|
|
payload: localPayload,
|
|
|
|
|
scene: this.currentChat.scene,
|
|
|
|
|
source_id: this.userInfo.id,
|
|
|
|
|
target_id: this.currentChat.id,
|
|
|
|
|
timestamp: 0,
|
|
|
|
|
type: type === "image" ? "image" : "file",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.$emit("send-message", {
|
|
|
|
|
...message,
|
|
|
|
|
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",
|
|
|
|
|
},
|
|
|
|
|
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,
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
this.$message.error("发送文件失败: " + e.message);
|
|
|
|
|
this.$emit("update-message-status", messageId, {
|
|
|
|
|
sending: false,
|
|
|
|
|
sendFailed: true,
|
|
|
|
|
isUploading: false,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
const { default: request } = await import("@/utils/request");
|
|
|
|
|
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)
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
generateFileName(originalName) {
|
|
|
|
|
const ext = originalName.split(".").pop();
|
|
|
|
|
const timestamp = Date.now();
|
|
|
|
|
const random = Math.random().toString(36).substr(2, 6);
|
|
|
|
|
return `${timestamp}_${random}.${ext}`;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
getObjectPath(fileName) {
|
|
|
|
|
const prefix =
|
|
|
|
|
this.currentChat.scene === ContactsScene.GROUP
|
|
|
|
|
? MinioPaths.FILE_MULTICHAT
|
|
|
|
|
: MinioPaths.FILE_PRIVATE;
|
|
|
|
|
return prefix + fileName;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
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] || "";
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
.message-editor {
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-top: 1px solid #ebeef5;
|
|
|
|
|
padding: 10px 16px;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.editor-toolbar {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
|
|
|
|
.el-button {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
color: #409eff;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.quill-wrap {
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.quill-container {
|
|
|
|
|
height: 100px;
|
|
|
|
|
padding-right: 100px;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
|
|
|
|
::v-deep .ql-container {
|
|
|
|
|
height: 100%;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
::v-deep .ql-editor {
|
|
|
|
|
min-height: 100%;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.send-area {
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: 8px;
|
|
|
|
|
bottom: 8px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
|
|
|
|
.send-mode-text {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #909399;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.at-panel {
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: 80px;
|
|
|
|
|
left: 20px;
|
|
|
|
|
width: 240px;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
|
|
z-index: 999;
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
|
|
|
|
|
.at-panel-header {
|
|
|
|
|
padding: 0 12px 8px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #909399;
|
|
|
|
|
border-bottom: 1px solid #ebeef5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.at-panel-list {
|
|
|
|
|
max-height: 200px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.at-panel-item {
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.at-panel-empty {
|
|
|
|
|
padding: 12px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|