系统设置-发送消息变更-发送消息则跟着变更

main
ysn 3 days ago
parent fe2eb7b5ad
commit 7c7589900d
  1. 31
      src/layout/components/SystemSettingDialog.vue
  2. 607
      src/views/message/components/MessageEditor.vue

@ -29,8 +29,8 @@
</el-form-item> -->
<el-form-item label="发送消息">
<el-radio-group v-model="basicForm.sendKey">
<el-radio label="ctrl">Ctrl+Enter发送</el-radio>
<el-radio label="enter">Enter发送</el-radio>
<el-radio label="Ctrl+Enter">Ctrl+Enter发送</el-radio>
<el-radio label="Enter">Enter发送</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="视讯存储">
@ -173,7 +173,7 @@ export default {
cacheAuthorized: false,
basicForm: {
notification: true,
sendKey: "enter",
sendKey: "Ctrl+Enter",
videoPath: "",
cachePath: "",
},
@ -215,6 +215,7 @@ export default {
const defaultPath = this.getDefaultPath();
if (savedSettings) {
const settings = JSON.parse(savedSettings);
let needSave = false;
//
if (settings.basicForm) {
this.basicForm = { ...this.basicForm, ...settings.basicForm };
@ -225,6 +226,11 @@ export default {
if (!this.basicForm.cachePath) {
this.basicForm.cachePath = defaultPath;
}
// 使 Ctrl+Enter
if (!this.basicForm.sendKey) {
this.basicForm.sendKey = "Ctrl+Enter";
needSave = true;
}
}
if (settings.audioForm) {
this.audioForm = { ...this.audioForm, ...settings.audioForm };
@ -232,10 +238,20 @@ export default {
if (settings.otherForm) {
this.otherForm = { ...this.otherForm, ...settings.otherForm };
}
//
if (!settings.version) {
this.basicForm.sendKey = "Ctrl+Enter";
needSave = true;
}
if (needSave) {
this.saveSettings();
}
} else {
// 使
// 使
this.basicForm.videoPath = defaultPath;
this.basicForm.cachePath = defaultPath;
this.basicForm.sendKey = "Ctrl+Enter";
this.saveSettings();
}
} catch (e) {
console.error("加载系统设置失败:", e);
@ -245,11 +261,16 @@ export default {
saveSettings() {
try {
const settings = {
version: "1.0",
basicForm: { ...this.basicForm },
audioForm: { ...this.audioForm },
otherForm: { ...this.otherForm },
};
localStorage.setItem(SYSTEM_SETTINGS_KEY, JSON.stringify(settings));
const event = new CustomEvent('systemSettingsChanged', {
detail: { settings }
});
window.dispatchEvent(event);
} catch (e) {
console.error("保存系统设置失败:", e);
}
@ -400,7 +421,7 @@ export default {
this.cacheAuthorized = false;
this.basicForm = {
notification: true,
sendKey: "enter",
sendKey: "Ctrl+Enter",
videoPath: defaultPath,
cachePath: defaultPath,
};

@ -125,7 +125,8 @@
ref="fileInput"
type="file"
style="display: none"
@change="(e) => handleFileChange(e, 'file')"
accept="image/*,video/*,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.zip,.rar,.txt"
@change="(e) => handleFileChange(e)"
/>
</div>
</template>
@ -159,7 +160,7 @@ export default {
currentRange: null,
sending: false,
maxFileSize: 100 * 1024 * 1024,
sendMode: "enter",
sendMode: this.getSystemSendMode(),
fontSizeMap: {
10: "small",
11: "small",
@ -170,6 +171,11 @@ export default {
// @
selectedAtUsers: [],
meetingModes: meetingModes(),
// Quill
quillInitRetryCount: 0,
//
editorFiles: [],
maxQuillInitRetries: 20,
};
},
@ -190,40 +196,104 @@ export default {
this.$nextTick(() => {
this.initQuill();
});
//
document.addEventListener("keydown", this.handleGlobalKeydown);
window.addEventListener("systemSettingsChanged", this.handleSettingsChanged);
this.loadSendModeFromSettings();
},
beforeDestroy() {
if (this.quill) {
this.quill = null;
}
//
document.removeEventListener("keydown", this.handleGlobalKeydown);
window.removeEventListener("systemSettingsChanged", this.handleSettingsChanged);
},
watch: {
sendMode(newMode) {
this.saveSendModeToSettings(newMode);
},
},
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) {
console.warn("Quill container not found, retrying...");
setTimeout(() => {
this.initQuill();
}, 100);
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;
}
//
if (this.quill) {
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,
});
@ -233,49 +303,103 @@ export default {
toolbar.container.style.display = "none";
}
//
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);
//
this.quill.root.addEventListener("keydown", (e) => {
if (this.sendMode === "enter") {
// EnterEnterCtrl+Enter
if (e.key === "Enter" && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
this.handleSendText();
}
} else {
// Ctrl+EnterCtrl+EnterEnter
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.handleSendText();
}
}
});
// ========= keydown bindings =========
},
// sendMode Quill
getQuillKeyBindings() {
const self = this;
if (this.sendMode === "enter") {
// EnterEnter=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+EnterEnter=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();
},
handleGlobalKeydown(e) {
// Ctrl+Enter / Command+EnterMac
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();
}
@ -323,6 +447,7 @@ export default {
if (this.quill) {
this.quill.setText("");
}
this.editorFiles = [];
},
// -
@ -333,6 +458,8 @@ export default {
//
handleSendModeChange(mode) {
this.sendMode = mode;
//
this.refreshQuillBindings();
},
//
@ -345,24 +472,32 @@ export default {
try {
await takeScreenshot(
async (blob) => {
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;
this.insertImageToEditor(uploadedPath);
this.$message.success("截图已插入到编辑器");
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("已取消截图");
@ -375,8 +510,6 @@ export default {
// ========== @ ==========
handleAtSelect(user) {
console.log(user);
if (!this.quill || !user) return;
const index = this.currentRange
? this.currentRange.index
@ -433,7 +566,14 @@ export default {
const html = this.quill.root.innerHTML;
const textContent = this.quill.getText().trim();
if (!textContent) return;
//
const hasText = textContent.replace(/\s+/g, "").length > 0;
const files = this.extractFilesFromEditor();
if (!hasText && files.length === 0) {
this.$message.warning("请输入内容或上传文件");
return;
}
this.sending = true;
@ -476,13 +616,13 @@ export default {
}
},
//
//
parseEditorContent(html, textContent) {
const messages = [];
const atUserIds = this.extractAtUsers(html);
const textOnly = textContent.replace(/\s+/g, "").length > 0;
const images = this.extractImagesFromEditor();
const files = this.extractFilesFromEditor();
if (textOnly) {
messages.push({
@ -505,67 +645,86 @@ export default {
});
}
images.forEach((image) => {
files.forEach((file) => {
const msgType = this.getMessageType(file.type);
messages.push({
at_users: [],
message_id: 0,
payload: {
content: "",
file_duration: 0,
file_ico: "",
file_name: image.fileName,
file_path: image.filePath,
file_size: image.fileSize || 0,
file_type: "image",
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,
target_id: this.currentChat.id,
timestamp: 0,
type: "image",
type: msgType,
});
});
return messages;
},
//
extractImagesFromEditor() {
const images = [];
if (!this.quill) return images;
//
extractFilesFromEditor() {
const files = [];
if (!this.quill) return files;
const config = this.$store.getters.config;
const endpoint = config.MINIO_ENDPOINT_HTTPS || "";
const bucket = config.MINIO_BUCKET_FILE || "remote-file-test";
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();
images.push({
src: src,
filePath: filePath,
fileName: fileName,
files.push({
name: fileName,
path: filePath,
type: "image",
size: 0,
icon: "",
});
}
});
return images;
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";
}
},
//
async handleFileChange(e, type) {
async handleFileChange(e) {
const files = e.target.files;
if (!files || !files.length) return;
const file = files[0];
e.target.value = "";
if (!file.type.startsWith("image/")) {
this.$message.warning("只能上传图片文件");
return;
}
if (file.size > this.maxFileSize) {
this.$message.warning("文件大小不能超过100MB");
return;
@ -593,12 +752,104 @@ export default {
);
const uploadedPath = bucket + "/" + result.objectName;
this.insertImageToEditor(uploadedPath);
this.$message.success("图片上传成功");
//
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.$message.error("发送文件失败: " + e.message);
this.$emit("update-message-status", messageId, {
sending: false,
sendFailed: true,
});
}
},
//
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";
},
async handlePaste(e) {
const items = e.clipboardData.items;
@ -728,6 +979,103 @@ export default {
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) {
@ -852,6 +1200,14 @@ export default {
::v-deep .ql-editor {
min-height: 100%;
padding: 8px;
img {
max-width: 100%;
height: auto;
display: block;
margin: 4px 0;
border-radius: 4px;
}
}
::v-deep .ql-toolbar {
@ -878,4 +1234,89 @@ export default {
}
}
}
.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>
Loading…
Cancel
Save