SWX\10484 3 days ago
commit 4146946fdd
  1. 2
      src/layout/components/ESignatureDialog.vue
  2. 31
      src/layout/components/SystemSettingDialog.vue
  3. 191
      src/views/knowledge/index.vue
  4. 8
      src/views/message/components/GroupSetting.vue
  5. 5
      src/views/message/components/MessageDisplay.vue
  6. 639
      src/views/message/components/MessageEditor.vue

@ -96,7 +96,7 @@ export default {
//
this.ctx.strokeStyle = "#000000";
this.ctx.lineWidth = 2;
this.ctx.lineWidth = 8;
this.ctx.lineCap = "round";
this.ctx.lineJoin = "round";

@ -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,
};

@ -171,7 +171,7 @@
v-show="queryRightParams.total > 0"
:total="queryRightParams.total"
:page.sync="queryRightParams.page"
:limit.sync="queryRightParams.pageSize"
:limit.sync="queryRightParams.size"
@pagination="getKnowledgeList"
/>
</el-card>
@ -343,7 +343,7 @@ export default {
cate_id: 0,
is_mine: "-1",
page: 1,
pageSize: 10,
size: 10,
total: 0,
},
// -
@ -572,32 +572,121 @@ export default {
? row.file_name.split(".").pop().toLowerCase()
: "";
if (videoTypes.includes(fileType)) {
//
const videoUrl = this.getFullFilePath(row.file_path);
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>${row.title || "视频播放"}</title>
<style>
*{margin:0;padding:0;}
html,body{width:100%;height:100%;background:#000;overflow:hidden;}
video{width:100%;height:100%;object-fit:cover;outline:none;}
</style>
</head>
<body>
<video src="${videoUrl}" controls autoplay></video>
</body>
</html>`;
const blob = new Blob([html], { type: "text/html;charset=utf-8" });
const blobUrl = URL.createObjectURL(blob);
postKnowledgePlay({ knowledge_id: row.id });
window.open(blobUrl, "_blank");
postKnowledgePlay({ knowledge_id: row.id }).then((res) => {
if (res.code === 200) {
// 使
const videoUrl = this.getFullFilePath(row.file_path);
//
this.createVideoPlayer(videoUrl, row.title || "视频播放");
}
})
} else {
postKnowledgePlay({ knowledge_id: row.id }).then((res) => {
window.open(this.getFullFilePath(row.file_path), "_blank");
});
if (res.code === 200) {
window.open(this.getFullFilePath(row.file_path), "_blank");
}
})
}
},
//
createVideoPlayer(videoUrl, title) {
try {
//
const supportsStreaming = this.checkStreamingSupport();
if (supportsStreaming) {
// URL
// 使
const playerWindow = window.open("", "_blank");
if (playerWindow) {
playerWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>${title}</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;}
html,body{width:100%;height:100%;background:#000;display:flex;flex-direction:column;}
.header{padding:10px 20px;background:#1a1a1a;color:#fff;display:flex;justify-content:space-between;align-items:center;}
.header h1{font-size:16px;font-weight:normal;}
.video-container{flex:1;display:flex;align-items:center;justify-content:center;padding:20px;}
video{max-width:100%;max-height:100%;background:#000;}
.error-message{color:#ff4444;padding:20px;text-align:center;}
.loading{color:#fff;padding:20px;text-align:center;}
.help-text{color:#888;font-size:12px;margin-top:10px;}
</style>
</head>
<body>
<div class="header"><h1>${title}</h1><span style="color:#888;font-size:12px;">正在加载...</span></div>
<div class="video-container">
<video id="player" controls preload="metadata" playsinline>
<source src="${videoUrl}" type="video/mp4">
<p class="error-message">您的浏览器不支持此视频格式</p>
</video>
</div>
<script>
var player = document.getElementById('player');
var header = document.querySelector('.header span');
player.addEventListener('loadedmetadata', function() {
header.textContent = '时长: ' + formatTime(player.duration);
});
player.addEventListener('timeupdate', function() {
header.textContent = formatTime(player.currentTime) + ' / ' + formatTime(player.duration);
});
player.addEventListener('error', function(e) {
console.error('视频播放错误:', e);
document.querySelector('.video-container').innerHTML = '<p class="error-message">视频加载失败,请尝试使用其他浏览器或下载后观看</p><p class="help-text">错误类型: ' + player.error?.code + '</p>';
});
player.addEventListener('stalled', function() {
header.textContent = '缓冲中...';
});
player.addEventListener('waiting', function() {
header.textContent = '等待数据...';
});
player.addEventListener('playing', function() {
header.textContent = formatTime(player.currentTime) + ' / ' + formatTime(player.duration);
});
function formatTime(seconds) {
if (isNaN(seconds)) return '--:--';
var h = Math.floor(seconds / 3600);
var m = Math.floor((seconds % 3600) / 60);
var s = Math.floor(seconds % 60);
return (h > 0 ? h + ':' : '') + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
}
<\/script>
</body>
</html>
`);
playerWindow.document.close();
}
} else {
// 退
this.$modal.msgWarning("您的浏览器可能不支持视频流媒体播放,正在尝试直接打开...");
window.open(videoUrl, "_blank");
}
} catch (error) {
console.error("创建视频播放器失败:", error);
// 退URL
window.open(this.getFullFilePath(row?.file_path || videoUrl), "_blank");
}
},
//
checkStreamingSupport() {
try {
const video = document.createElement('video');
// MSE
return !!window.MediaSource || video.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"') === 'probably';
} catch (e) {
return true; //
}
},
handleUpdateFileName(row) {
@ -695,17 +784,17 @@ export default {
if (!file) return;
//
// const validateResult = this.validateFileName(file.name);
// if (!validateResult.valid) {
// this.$modal.msgError(validateResult.message);
// event.target.value = "";
// return;
// }
const validateResult = this.validateFileName(file.name);
if (!validateResult.valid) {
this.$modal.confirm(validateResult.message);
event.target.value = "";
return;
}
//
// const sizeResult = this.validateFileSize(file.size);
// if (!sizeResult.valid) {
// this.$modal.msgError(sizeResult.message);
// this.$modal.confirm(sizeResult.message);
// event.target.value = "";
// return;
// }
@ -725,43 +814,17 @@ export default {
},
//
validateFileName(fileName) {
// 1.
if (!fileName || fileName.trim() === "") {
return { valid: false, message: "文件名不能为空" };
}
// 2. 20
const nameWithoutExt = fileName.replace(/\.[^/.]+$/, "");
if (nameWithoutExt.length > 20) {
return {
valid: false,
message: "文件名(不含扩展名)不能超过20个字符",
};
}
// 3.
if (/^\d/.test(nameWithoutExt)) {
return { valid: false, message: "文件名不能以数字开头" };
}
// 4.
const specialChars = /^[!@#$%^&*()_+\-=\[\]{}|;:,.<>?~`]/;
if (specialChars.test(nameWithoutExt)) {
return { valid: false, message: "文件名不能以特殊字符开头" };
}
// 5. Windows
const invalidChars = /[\\/:*?"<>|]/;
// @#$%^
const invalidChars = /[\\/:*?"<>|@#$%^&()\[\] ]/;
if (invalidChars.test(fileName)) {
return {
valid: false,
message: '文件名不能包含 \\ / : * ? " < > | 等特殊字符',
message: `文件名不能包含空格、@、#、$、%、^等特殊字符`,
};
}
return { valid: true, message: "" };
},
//
//
validateFileSize(size) {
// 100MB
const maxSize = 100 * 1024 * 1024;
@ -864,7 +927,7 @@ export default {
thumbnail_path:
fullThumbnailPath ||
"personal-test/video/688/1780036088/1633500241136.png",
title: file.name.replace(/\.[^/.]+$/, ""), //
title: file.name, //
true_file_size: file.size,
};
await postKnowledgeCreate(data);

@ -68,7 +68,7 @@
<i class="el-icon-minus" />
</span>
</div>
<span class="member-name">{{ member.name }}</span>
<span class="member-name" :title="member.name">{{ member.name }}</span>
</div>
</div>
</div>
@ -93,7 +93,7 @@
member.avatar
"
/>
<span class="member-name">{{ member.name }}</span>
<span class="member-name" :title="member.name">{{ member.name }}</span>
</div>
</div>
</div>
@ -369,6 +369,10 @@ export default {
font-size: 12px;
color: #606266;
text-align: center;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.admin-tag {

@ -423,9 +423,10 @@ export default {
await this.$nextTick();
await this.preloadHistoryMessages();
this.$nextTick(() => {
// 使 setTimeout DOM
setTimeout(() => {
this.scrollToBottom();
});
}, 100);
this.markAsRead();
},

@ -20,13 +20,16 @@
title="截屏"
/>
<el-dropdown @command="handleFontSizeChange">
<el-button type="text" style="font-size: 18px">A</el-button>
<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-item
v-for="size in [10, 11, 12, 13, 14]"
:key="size"
:command="String(size)"
:class="{ 'font-size-selected': currentFontSize === size }"
>
{{ size }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-dropdown
@ -125,7 +128,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 +163,7 @@ export default {
currentRange: null,
sending: false,
maxFileSize: 100 * 1024 * 1024,
sendMode: "enter",
sendMode: this.getSystemSendMode(),
fontSizeMap: {
10: "small",
11: "small",
@ -167,9 +171,15 @@ export default {
13: "large",
14: "large",
},
currentFontSize: 14,
// @
selectedAtUsers: [],
meetingModes: meetingModes(),
// Quill
quillInitRetryCount: 0,
//
editorFiles: [],
maxQuillInitRetries: 20,
};
},
@ -190,40 +200,110 @@ 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 +313,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,16 +457,20 @@ export default {
if (this.quill) {
this.quill.setText("");
}
this.editorFiles = [];
},
// -
handleFontSizeChange(size) {
this.$emit("font-size-change", parseInt(size));
this.currentFontSize = parseInt(size);
this.$emit("font-size-change", this.currentFontSize);
},
//
handleSendModeChange(mode) {
this.sendMode = mode;
//
this.refreshQuillBindings();
},
//
@ -345,24 +483,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 +521,6 @@ export default {
// ========== @ ==========
handleAtSelect(user) {
console.log(user);
if (!this.quill || !user) return;
const index = this.currentRange
? this.currentRange.index
@ -433,7 +577,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 +627,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 +656,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 +763,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 +990,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 +1211,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 +1245,96 @@ 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;
}
}
.el-dropdown-menu {
.font-size-selected {
color: #009393;
font-weight: 500;
}
}
</style>
Loading…
Cancel
Save