消息消息模块-功能联调

main
ysn 2 weeks ago
parent 30af97e1ee
commit ed66c64bce
  1. 309
      src/utils/screenshot.js
  2. 29
      src/views/message/components/MessageDisplay.vue
  3. 738
      src/views/message/components/MessageEditor.vue
  4. 21
      src/views/message/index.vue

@ -23,6 +23,9 @@ class ScreenshotTool {
this.annotations = []; this.annotations = [];
this.textInput = null; this.textInput = null;
this.dpr = window.devicePixelRatio || 1; this.dpr = window.devicePixelRatio || 1;
this.lineWidth = 2;
this.lineColor = '#ff4d4f';
} }
async start(onComplete, onCancel) { async start(onComplete, onCancel) {
@ -30,23 +33,23 @@ class ScreenshotTool {
this.onCancel = onCancel; this.onCancel = onCancel;
if (!this.isSupported()) { if (!this.isSupported()) {
throw new Error("当前浏览器不支持截图"); throw new Error('当前浏览器不支持截图');
} }
try { try {
this.stream = await navigator.mediaDevices.getDisplayMedia({ this.stream = await navigator.mediaDevices.getDisplayMedia({
video: { displaySurface: "monitor", width: 3840, height: 2160 }, video: { displaySurface: 'monitor', width: 3840, height: 2160 },
audio: false, audio: false,
}); });
this.video = document.createElement("video"); this.video = document.createElement('video');
this.video.srcObject = this.stream; this.video.srcObject = this.stream;
this.video.muted = true; this.video.muted = true;
await this.video.play(); await this.video.play();
this.createOverlay(); this.createOverlay();
} catch (err) { } catch (err) {
if (err.name === "NotAllowedError") throw new Error("已拒绝屏幕权限"); if (err.name === 'NotAllowedError') throw new Error('已拒绝屏幕权限');
throw new Error("截图启动失败"); throw new Error('截图启动失败');
} }
} }
@ -57,14 +60,14 @@ class ScreenshotTool {
createOverlay() { createOverlay() {
this.scrollX = window.scrollX; this.scrollX = window.scrollX;
this.scrollY = window.scrollY; this.scrollY = window.scrollY;
document.body.style.overflow = "hidden"; document.body.style.overflow = 'hidden';
document.documentElement.style.overflow = "hidden"; document.documentElement.style.overflow = 'hidden';
this.overlay = document.createElement("div"); this.overlay = document.createElement('div');
this.overlay.id = "screenshot-overlay"; this.overlay.id = 'screenshot-overlay';
this.overlay.style.cssText = ` this.overlay.style.cssText = `
position:fixed;top:0;left:0;width:100vw;height:100vh; position:fixed;top:0;left:0;width:100vw;height:100vh;
background:rgba(0,0,0,0.6);z-index:999999;cursor:crosshair;user-select:none; background:rgba(0,0,0,0.5);z-index:999999;cursor:crosshair;user-select:none;
`; `;
document.body.appendChild(this.overlay); document.body.appendChild(this.overlay);
@ -77,78 +80,118 @@ class ScreenshotTool {
const w = this.video.videoWidth; const w = this.video.videoWidth;
const h = this.video.videoHeight; const h = this.video.videoHeight;
this.canvas = document.createElement("canvas"); this.canvas = document.createElement('canvas');
this.canvas.width = w; this.canvas.width = w;
this.canvas.height = h; this.canvas.height = h;
this.canvas.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%"; this.canvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%';
this.overlay.appendChild(this.canvas); this.overlay.appendChild(this.canvas);
this.canvas.getContext("2d").drawImage(this.video, 0, 0, w, h); this.canvas.getContext('2d').drawImage(this.video, 0, 0, w, h);
this.selectionCanvas = document.createElement("canvas"); this.selectionCanvas = document.createElement('canvas');
this.selectionCanvas.width = w; this.selectionCanvas.width = w;
this.selectionCanvas.height = h; this.selectionCanvas.height = h;
this.selectionCanvas.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none"; this.selectionCanvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none';
this.overlay.appendChild(this.selectionCanvas); this.overlay.appendChild(this.selectionCanvas);
this.drawCanvas = document.createElement("canvas"); this.drawCanvas = document.createElement('canvas');
this.drawCanvas.width = w; this.drawCanvas.width = w;
this.drawCanvas.height = h; this.drawCanvas.height = h;
this.drawCanvas.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;display:none"; this.drawCanvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;display:none';
this.overlay.appendChild(this.drawCanvas); this.overlay.appendChild(this.drawCanvas);
} }
createToolbar() { createToolbar() {
this.toolbar = document.createElement("div"); this.toolbar = document.createElement('div');
this.toolbar.style.cssText = ` this.toolbar.style.cssText = `
position:fixed;bottom:60px;left:50%;transform:translateX(-50%); position:fixed;top:40px;left:50%;transform:translateX(-50%);
background:#1f1f1f;border-radius:10px;padding:10px 20px; background:#fff;border-radius:8px;padding:6px;
display:flex;gap:28px;box-shadow:0 4px 20px rgba(0,0,0,0.4);z-index:1000000; display:flex;gap:4px;box-shadow:0 2px 12px rgba(0,0,0,0.15);z-index:1000000;
`; `;
const btns = [ const tools = [
{ icon: "✓", color: "#67c23a", action: "confirm" }, { icon: '▢', tool: 'rect', title: '矩形' },
{ icon: "✕", color: "#f56c6c", action: "cancel" }, { icon: '○', tool: 'circle', title: '圆形' },
{ icon: "🖵", color: "#409eff", action: "fullscreen" }, { icon: '✏', tool: 'pen', title: '画笔' },
{ icon: '➤', tool: 'arrow', title: '箭头' },
{ icon: 'A', tool: 'text', title: '文字' },
{ icon: '▦', tool: 'mosaic', title: '马赛克' },
{ icon: '↩', tool: 'undo', title: '撤销' },
{ icon: '🖵', tool: 'fullscreen', title: '全屏' },
]; ];
btns.forEach((b) => { tools.forEach((t) => {
const el = document.createElement("button"); const el = document.createElement('button');
el.innerHTML = `<span style="font-size:20px">${b.icon}</span>`; el.innerHTML = `<span style="font-size:18px">${t.icon}</span>`;
el.style.cssText = `background:none;border:none;color:${b.color};cursor:pointer;padding:6px;border-radius:6px`; el.style.cssText = `
el.onmouseover = () => el.style.background = "rgba(255,255,255,0.1)"; background:none;border:none;color:#333;cursor:pointer;
el.onmouseout = () => el.style.background = "transparent"; padding:8px 12px;border-radius:6px;
el.onclick = (e) => { e.stopPropagation(); this.handleAction(b.action); }; display:flex;align-items:center;justify-content:center;
min-width:36px;height:36px;
transition:background-color 0.2s;
`;
el.title = t.title;
el.onmouseover = () => { el.style.background = '#f0f0f0'; };
el.onmouseout = () => { el.style.background = 'transparent'; };
el.onclick = (e) => {
e.stopPropagation();
this.handleToolbarTool(t.tool);
};
this.toolbar.appendChild(el); this.toolbar.appendChild(el);
}); });
this.overlay.appendChild(this.toolbar); this.overlay.appendChild(this.toolbar);
this.actionToolbar = document.createElement("div"); this.actionToolbar = document.createElement('div');
this.actionToolbar.style.cssText = ` this.actionToolbar.style.cssText = `
position:fixed;background:#fff;border-radius:8px;padding:6px 10px; position:fixed;bottom:40px;left:50%;transform:translateX(-50%);
display:none;gap:12px;box-shadow:0 2px 12px rgba(0,0,0,0.15);z-index:1000001; background:#fff;border-radius:8px;padding:6px;
display:flex;gap:4px;box-shadow:0 2px 12px rgba(0,0,0,0.15);z-index:1000001;
`; `;
const tools = [ const actions = [
{ icon: "▢", tool: "rect" }, { icon: "○", tool: "circle" }, { icon: '✕', action: 'cancel', color: '#666', title: '取消' },
{ icon: "✏", tool: "pen" }, { icon: "➤", tool: "arrow" }, { icon: '✓', action: 'confirm', color: '#52c41a', title: '确认' },
{ icon: "A", tool: "text" }, { icon: "▦", tool: "mosaic" },
{ icon: "↩", tool: "undo" },
]; ];
tools.forEach((t) => { actions.forEach((a) => {
const el = document.createElement("button"); const el = document.createElement('button');
el.innerHTML = `<span style="font-size:16px">${t.icon}</span>`; el.innerHTML = `<span style="font-size:20px">${a.icon}</span>`;
el.style.cssText = "background:none;border:none;color:#333;cursor:pointer;padding:4px 6px;border-radius:4px"; el.style.cssText = `
el.onmouseover = () => el.style.background = "#f0f0f0"; background:none;border:none;color:${a.color};cursor:pointer;
el.onmouseout = () => el.style.background = "transparent"; padding:10px 20px;border-radius:6px;
el.onclick = (e) => { e.stopPropagation(); this.handleAnnotateTool(t.tool); }; font-weight:500;
transition:background-color 0.2s;
min-width:80px;height:36px;
display:flex;align-items:center;justify-content:center;
`;
el.title = a.title;
el.onmouseover = () => { el.style.background = '#f0f0f0'; };
el.onmouseout = () => { el.style.background = 'transparent'; };
el.onclick = (e) => {
e.stopPropagation();
this.handleAction(a.action);
};
this.actionToolbar.appendChild(el); this.actionToolbar.appendChild(el);
}); });
this.overlay.appendChild(this.actionToolbar); this.overlay.appendChild(this.actionToolbar);
} }
handleToolbarTool(tool) {
if (tool === 'undo') {
this.undoAnnotation();
return;
}
if (tool === 'fullscreen') {
this.captureFullscreen();
return;
}
this.currentTool = tool;
this.isAnnotating = true;
this.drawCanvas.style.display = 'block';
this.overlay.style.cursor = tool === 'text' ? 'text' : 'crosshair';
}
addEventListeners() { addEventListeners() {
this.overlay.addEventListener("mousedown", (e) => { this.overlay.addEventListener('mousedown', (e) => {
if (this.toolbar.contains(e.target) || this.actionToolbar.contains(e.target)) return; if (this.toolbar.contains(e.target) || this.actionToolbar.contains(e.target)) return;
if (this.isAnnotating && this.currentTool) { if (this.isAnnotating && this.currentTool) {
this.startAnnotation(e); this.startAnnotation(e);
@ -161,7 +204,7 @@ class ScreenshotTool {
} }
}); });
this.overlay.addEventListener("mousemove", (e) => { this.overlay.addEventListener('mousemove', (e) => {
if (this.isDrawing) { if (this.isDrawing) {
this.currentX = e.clientX; this.currentX = e.clientX;
this.currentY = e.clientY; this.currentY = e.clientY;
@ -171,65 +214,77 @@ class ScreenshotTool {
} }
}); });
this.overlay.addEventListener("mouseup", () => { this.overlay.addEventListener('mouseup', () => {
if (this.isDrawing) { if (this.isDrawing) {
this.isDrawing = false; this.isDrawing = false;
this.showActionToolbar(); const w = Math.abs(this.currentX - this.startX);
const h = Math.abs(this.currentY - this.startY);
if (w >= 15 && h >= 15) {
this.isAnnotating = true;
this.drawCanvas.style.display = 'block';
}
} else if (this.isDrawingAnnotation) { } else if (this.isDrawingAnnotation) {
this.finishAnnotation(); this.finishAnnotation();
} }
}); });
this.keydownHandler = (e) => e.key === "Escape" && this.cancel(); this.keydownHandler = (e) => e.key === 'Escape' && this.cancel();
document.addEventListener("keydown", this.keydownHandler); document.addEventListener('keydown', this.keydownHandler);
} }
updateSelection() { updateSelection() {
const ctx = this.selectionCanvas.getContext("2d"); const ctx = this.selectionCanvas.getContext('2d');
ctx.clearRect(0, 0, this.selectionCanvas.width, this.selectionCanvas.height); ctx.clearRect(0, 0, this.selectionCanvas.width, this.selectionCanvas.height);
const x = Math.min(this.startX, this.currentX); const x = Math.min(this.startX, this.currentX);
const y = Math.min(this.startY, this.currentY); const y = Math.min(this.startY, this.currentY);
const w = Math.abs(this.currentX - this.startX); const w = Math.abs(this.currentX - this.startX);
const h = Math.abs(this.currentY - this.startY); const h = Math.abs(this.currentY - this.startY);
ctx.fillStyle = "rgba(0,0,0,0.6)"; ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(0, 0, this.selectionCanvas.width, this.selectionCanvas.height); ctx.fillRect(0, 0, this.selectionCanvas.width, this.selectionCanvas.height);
ctx.clearRect(x, y, w, h); ctx.clearRect(x, y, w, h);
ctx.strokeStyle = "#409eff";
ctx.lineWidth = 2;
ctx.strokeRect(x, y, w, h);
}
showActionToolbar() { ctx.strokeStyle = '#fff';
const x = Math.min(this.startX, this.currentX); ctx.lineWidth = 1;
const y = Math.min(this.startY, this.currentY); ctx.strokeRect(x, y, w, h);
const w = Math.abs(this.currentX - this.startX);
const h = Math.abs(this.currentY - this.startY);
if (w < 15 || h < 15) return;
this.isAnnotating = true; ctx.strokeStyle = '#52c41a';
this.drawCanvas.style.display = "block"; ctx.lineWidth = 2;
this.actionToolbar.style.display = "flex"; ctx.strokeRect(x + 1, y + 1, w - 2, h - 2);
this.actionToolbar.style.left = `${x + w - 220}px`;
this.actionToolbar.style.top = `${y + h + 10}px`; this.drawCorner(ctx, x, y, 12);
this.drawCorner(ctx, x + w, y, 12);
this.drawCorner(ctx, x, y + h, 12);
this.drawCorner(ctx, x + w, y + h, 12);
ctx.fillStyle = '#52c41a';
ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
const sizeText = `${Math.round(w)} × ${Math.round(h)}`;
ctx.fillText(sizeText, x + 8, y - 20);
} }
handleAnnotateTool(tool) { drawCorner(ctx, x, y, size) {
if (tool === "undo") return this.undoAnnotation(); ctx.strokeStyle = '#52c41a';
this.currentTool = tool; ctx.lineWidth = 2;
this.overlay.style.cursor = tool === "text" ? "text" : "crosshair"; ctx.beginPath();
ctx.moveTo(x - size, y);
ctx.lineTo(x, y);
ctx.lineTo(x, y - size);
ctx.stroke();
} }
startAnnotation(e) { startAnnotation(e) {
this.isDrawingAnnotation = true; this.isDrawingAnnotation = true;
this.annotStartX = e.clientX; this.annotStartX = e.clientX;
this.annotStartY = e.clientY; this.annotStartY = e.clientY;
if (this.currentTool === "text") this.createTextInput(e.clientX, e.clientY); if (this.currentTool === 'text') this.createTextInput(e.clientX, e.clientY);
} }
continueAnnotation(e) { continueAnnotation(e) {
if (!this.isDrawingAnnotation || !this.currentTool || this.currentTool === "text") return; if (!this.isDrawingAnnotation || !this.currentTool || this.currentTool === 'text') return;
const ctx = this.drawCanvas.getContext("2d"); const ctx = this.drawCanvas.getContext('2d');
ctx.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height); ctx.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height);
this.redrawAnnotations(ctx); this.redrawAnnotations(ctx);
@ -237,61 +292,88 @@ class ScreenshotTool {
const y = this.annotStartY; const y = this.annotStartY;
const ex = e.clientX; const ex = e.clientX;
const ey = e.clientY; const ey = e.clientY;
ctx.strokeStyle = "#ff4d4f"; ctx.strokeStyle = this.lineColor;
ctx.fillStyle = "#ff4d4f"; ctx.fillStyle = this.lineColor;
ctx.lineWidth = 2; ctx.lineWidth = this.lineWidth;
switch (this.currentTool) { switch (this.currentTool) {
case "rect": ctx.strokeRect(x, y, ex - x, ey - y); break; case 'rect':
case "circle": ctx.beginPath(); ctx.arc(x, y, Math.hypot(ex - x, ey - y), 0, Math.PI * 2); ctx.stroke(); break; ctx.strokeRect(x, y, ex - x, ey - y);
case "pen": ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(ex, ey); ctx.stroke(); this.annotStartX = ex; this.annotStartY = ey; break; break;
case "arrow": this.drawArrow(ctx, x, y, ex, ey); break; case 'circle':
case "mosaic": this.drawMosaic(ctx, ex, ey); break; ctx.beginPath();
ctx.arc(x, y, Math.hypot(ex - x, ey - y), 0, Math.PI * 2);
ctx.stroke();
break;
case 'pen':
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(ex, ey);
ctx.stroke();
this.annotStartX = ex;
this.annotStartY = ey;
break;
case 'arrow':
this.drawArrow(ctx, x, y, ex, ey);
break;
case 'mosaic':
this.drawMosaic(ctx, ex, ey);
break;
} }
} }
finishAnnotation() { finishAnnotation() {
if (!this.isDrawingAnnotation || !this.currentTool || this.currentTool === "text") return; if (!this.isDrawingAnnotation || !this.currentTool || this.currentTool === 'text') return;
this.isDrawingAnnotation = false; this.isDrawingAnnotation = false;
const ctx = this.drawCanvas.getContext("2d"); const ctx = this.drawCanvas.getContext('2d');
this.annotations.push(ctx.getImageData(0, 0, this.drawCanvas.width, this.drawCanvas.height)); this.annotations.push(ctx.getImageData(0, 0, this.drawCanvas.width, this.drawCanvas.height));
} }
drawArrow(ctx, x1, y1, x2, y2) { drawArrow(ctx, x1, y1, x2, y2) {
const head = 15; const head = 12;
const angle = Math.atan2(y2 - y1, x2 - x1); const angle = Math.atan2(y2 - y1, x2 - x1);
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x2, y2); ctx.moveTo(x2, y2);
ctx.lineTo(x2 - head * Math.cos(angle - Math.PI / 6), y2 - head * Math.sin(angle - Math.PI / 6)); ctx.lineTo(x2 - head * Math.cos(angle - Math.PI / 6), y2 - head * Math.sin(angle - Math.PI / 6));
ctx.lineTo(x2 - head * Math.cos(angle + Math.PI / 6), y2 - head * Math.sin(angle + Math.PI / 6)); ctx.lineTo(x2 - head * Math.cos(angle + Math.PI / 6), y2 - head * Math.sin(angle + Math.PI / 6));
ctx.closePath(); ctx.fill(); ctx.closePath();
ctx.fill();
} }
drawMosaic(ctx, x, y) { drawMosaic(ctx, x, y) {
const s = 8; const s = 8;
const data = ctx.getImageData(x - s / 2, y - s / 2, s, s); const rectX = Math.floor(x / s) * s;
const r = data.data[0], g = data.data[1], b = data.data[2]; const rectY = Math.floor(y / s) * s;
const data = ctx.getImageData(rectX, rectY, s, s);
const r = data.data[0];
const g = data.data[1];
const b = data.data[2];
ctx.fillStyle = `rgb(${r},${g},${b})`; ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(x - s / 2, y - s / 2, s, s); ctx.fillRect(rectX, rectY, s, s);
} }
createTextInput(x, y) { createTextInput(x, y) {
if (this.textInput) this.textInput.remove(); if (this.textInput) this.textInput.remove();
this.textInput = document.createElement("input"); this.textInput = document.createElement('input');
this.textInput.style.cssText = ` this.textInput.style.cssText = `
position:fixed;left:${x}px;top:${y}px;border:none;outline:none; position:fixed;left:${x}px;top:${y}px;border:none;outline:none;
font-size:16px;color:#ff4d4f;background:transparent;z-index:1000002;min-width:100px; font-size:16px;color:#ff4d4f;background:transparent;z-index:1000002;min-width:100px;
`; `;
this.textInput.onkeydown = (e) => { this.textInput.onkeydown = (e) => {
if (e.key === "Enter") { if (e.key === 'Enter') {
this.addText(x, y, this.textInput.value); this.addText(x, y, this.textInput.value);
this.textInput.remove(); this.textInput = null; this.textInput.remove();
this.textInput = null;
} }
}; };
this.textInput.onblur = () => { this.textInput.onblur = () => {
if (this.textInput.value) this.addText(x, y, this.textInput.value); if (this.textInput.value) this.addText(x, y, this.textInput.value);
this.textInput?.remove(); this.textInput = null; this.textInput?.remove();
this.textInput = null;
}; };
this.overlay.appendChild(this.textInput); this.overlay.appendChild(this.textInput);
this.textInput.focus(); this.textInput.focus();
@ -299,30 +381,33 @@ class ScreenshotTool {
addText(x, y, text) { addText(x, y, text) {
if (!text) return; if (!text) return;
const ctx = this.drawCanvas.getContext("2d"); const ctx = this.drawCanvas.getContext('2d');
ctx.font = "16px Arial"; ctx.font = '16px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
ctx.fillStyle = "#ff4d4f"; ctx.fillStyle = '#ff4d4f';
ctx.fillText(text, x, y + 16); ctx.fillText(text, x, y + 16);
this.annotations.push(ctx.getImageData(0, 0, this.drawCanvas.width, this.drawCanvas.height)); this.annotations.push(ctx.getImageData(0, 0, this.drawCanvas.width, this.drawCanvas.height));
} }
redrawAnnotations(ctx) { redrawAnnotations(ctx) {
this.annotations.forEach(d => ctx.putImageData(d, 0, 0)); this.annotations.forEach((d) => ctx.putImageData(d, 0, 0));
} }
undoAnnotation() { undoAnnotation() {
if (this.annotations.length === 0) return; if (this.annotations.length === 0) return;
this.annotations.pop(); this.annotations.pop();
const ctx = this.drawCanvas.getContext("2d"); const ctx = this.drawCanvas.getContext('2d');
ctx.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height); ctx.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height);
this.redrawAnnotations(ctx); this.redrawAnnotations(ctx);
} }
handleAction(a) { handleAction(a) {
switch (a) { switch (a) {
case "confirm": this.confirm(); break; case 'confirm':
case "cancel": this.cancel(); break; this.confirm();
case "fullscreen": this.captureFullscreen(); break; break;
case 'cancel':
this.cancel();
break;
} }
} }
@ -339,10 +424,10 @@ class ScreenshotTool {
} }
capture(cx, cy, cw, ch) { capture(cx, cy, cw, ch) {
const out = document.createElement("canvas"); const out = document.createElement('canvas');
out.width = cw * this.dpr; out.width = cw * this.dpr;
out.height = ch * this.dpr; out.height = ch * this.dpr;
const ctx = out.getContext("2d"); const ctx = out.getContext('2d');
ctx.scale(this.dpr, this.dpr); ctx.scale(this.dpr, this.dpr);
const vw = this.video.videoWidth; const vw = this.video.videoWidth;
@ -354,7 +439,7 @@ class ScreenshotTool {
ctx.drawImage(this.drawCanvas, cx * sx, cy * sy, cw * sx, ch * sy, 0, 0, cw, ch); ctx.drawImage(this.drawCanvas, cx * sx, cy * sy, cw * sx, ch * sy, 0, 0, cw, ch);
this.cleanup(); this.cleanup();
out.toBlob(b => this.onComplete?.(b), "image/png", 1); out.toBlob((b) => this.onComplete?.(b), 'image/png', 1);
} }
cancel() { cancel() {
@ -363,13 +448,13 @@ class ScreenshotTool {
} }
cleanup() { cleanup() {
document.body.style.overflow = ""; document.body.style.overflow = '';
document.documentElement.style.overflow = ""; document.documentElement.style.overflow = '';
window.scrollTo(this.scrollX, this.scrollY); window.scrollTo(this.scrollX, this.scrollY);
this.overlay?.remove(); this.overlay?.remove();
this.textInput?.remove(); this.textInput?.remove();
this.stream?.getTracks().forEach(t => t.stop()); this.stream?.getTracks().forEach((t) => t.stop());
document.removeEventListener("keydown", this.keydownHandler); document.removeEventListener('keydown', this.keydownHandler);
} }
} }

@ -5,7 +5,7 @@
<div class="chat-title"> <div class="chat-title">
<span class="name" v-if="currentChat">{{ currentChat.name }}</span> <span class="name" v-if="currentChat">{{ currentChat.name }}</span>
<span v-if="currentChat && currentChat.scene === ContactsScene.GROUP"> <span v-if="currentChat && currentChat.scene === ContactsScene.GROUP">
({{ currentChat.user_num }}) ({{ currentChat.user_num }})
</span> </span>
</div> </div>
<div class="chat-actions"> <div class="chat-actions">
@ -406,7 +406,8 @@ export default {
newChat && newChat &&
(!oldChat || (!oldChat ||
newChat.id !== oldChat.id || newChat.id !== oldChat.id ||
newChat.scene !== oldChat.scene) newChat.scene !== oldChat.scene ||
newChat.last_message_id !== oldChat.last_message_id)
) { ) {
this.switchToChat(newChat); this.switchToChat(newChat);
} }
@ -535,15 +536,19 @@ export default {
let merged; let merged;
// ID // ID
const existingIds = new Set(existing.map(m => m.message_id || m.id)); const existingIds = new Set(existing.map((m) => m.message_id || m.id));
if (up === 0) { if (up === 0) {
// //
const newMessages = processed.filter(m => !existingIds.has(m.message_id || m.id)); const newMessages = processed.filter(
(m) => !existingIds.has(m.message_id || m.id)
);
merged = [...existing, ...newMessages]; merged = [...existing, ...newMessages];
} else { } else {
// //
const newMessages = processed.filter(m => !existingIds.has(m.message_id || m.id)); const newMessages = processed.filter(
(m) => !existingIds.has(m.message_id || m.id)
);
merged = [...newMessages, ...existing]; merged = [...newMessages, ...existing];
} }
@ -658,7 +663,18 @@ export default {
formatTextContent(content) { formatTextContent(content) {
if (!content) return ""; if (!content) return "";
// content content
let text = content; let text = content;
if (typeof content === "object") {
text = content.content || content.payload || JSON.stringify(content);
}
// text
if (typeof text !== "string") {
text = String(text);
}
// HTML // HTML
text = text text = text
.replace(/&/g, "&amp;") .replace(/&/g, "&amp;")
@ -887,6 +903,7 @@ export default {
this.fontSize = fontSize; this.fontSize = fontSize;
const container = this.$refs.messageContainer; const container = this.$refs.messageContainer;
if (container) { if (container) {
container.style.setProperty("--message-font-size", fontSize + "px");
container.style.fontSize = fontSize + "px"; container.style.fontSize = fontSize + "px";
} }
}, },
@ -999,7 +1016,7 @@ export default {
padding: 10px 14px; padding: 10px 14px;
border-radius: 12px; border-radius: 12px;
word-break: break-word; word-break: break-word;
font-size: 14px; font-size: var(--message-font-size, 14px);
line-height: 1.5; line-height: 1.5;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);

@ -1,130 +1,125 @@
<template> <template>
<div class="message-editor"> <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">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 @command="handleAtSelect" v-if="currentChat.scene == 4&&groupInfo">
<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 编辑区 + 右下角发送按钮 容器 --> <!-- Quill 编辑区 + 右下角发送按钮 容器 -->
<div class="quill-wrap"> <div class="quill-wrap">
<div ref="quillEditor" class="quill-container"></div> <div ref="quillEditor" class="quill-container"></div>
<!-- 右下角发送区域 --> <!-- 右下角发送区域 -->
<div class="send-area"> <!-- 工具栏 -->
<el-button <div class="editor-toolbar">
type="primary" <div class="editor-toolbar-left">
size="small" <el-button
:disabled="!canSend" type="text"
:loading="sending" icon="el-icon-folder-opened"
@click="handleSendText" @click="handleFileClick"
> title="文件"
发送 />
</el-button> <el-button
<el-dropdown @command="handleSendModeChange"> type="text"
<el-button type="text" size="small"> icon="el-icon-scissors"
<span class="send-mode-text">{{ sendModeLabel }}</span> @click="handleScreenshotClick"
<i class="el-icon-arrow-down el-icon--right"></i> title="截屏"
</el-button> />
<el-dropdown-menu slot="dropdown"> <el-dropdown @command="handleFontSizeChange">
<el-dropdown-item command="enter">Enter发送</el-dropdown-item> <el-button type="text" style="font-size: 18px">A</el-button>
<el-dropdown-item command="ctrlEnter" <el-dropdown-menu slot="dropdown">
>Ctrl+Enter发送</el-dropdown-item <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-menu> <el-dropdown-item
</el-dropdown> :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>
</div> </div>
</div> </div>
<!-- 隐藏文件上传 --> <!-- 隐藏文件上传 -->
<input <input
ref="fileInput" ref="fileInput"
@ -141,13 +136,15 @@ import Quill from "quill";
import "quill/dist/quill.core.css"; import "quill/dist/quill.core.css";
import "quill/dist/quill.snow.css"; import "quill/dist/quill.snow.css";
import { sendPrivateMessage, sendMultiChatMessage } from "@/api/message"; import { sendPrivateMessage, sendMultiChatMessage } from "@/api/message";
import { uploadFile } from "@/utils/requestMinio";
import { takeScreenshot } from "@/utils/screenshot";
import { import {
MessageType, MessageType,
ContactsScene, ContactsScene,
MinioPaths, MinioPaths,
MqttTopics, MqttTopics,
} from "@/utils/constants"; } from "@/utils/constants";
import { meetingModes } from "@/api/videoCommunication";
export default { export default {
name: "MessageEditor", name: "MessageEditor",
props: { props: {
@ -156,6 +153,8 @@ export default {
data() { data() {
return { return {
MessageType,
ContactsScene,
quill: null, quill: null,
currentRange: null, currentRange: null,
sending: false, sending: false,
@ -170,11 +169,12 @@ export default {
}, },
// @ // @
selectedAtUsers: [], selectedAtUsers: [],
meetingModes: meetingModes(),
}; };
}, },
computed: { computed: {
...mapGetters(["currentChat", "userInfo"]), ...mapGetters(["currentChat", "userInfo", "config"]),
canSend() { canSend() {
const text = this.quill ? this.quill.getText().trim() : ""; const text = this.quill ? this.quill.getText().trim() : "";
@ -187,17 +187,38 @@ export default {
}, },
mounted() { mounted() {
this.initQuill(); this.$nextTick(() => {
this.initQuill();
});
//
document.addEventListener("keydown", this.handleGlobalKeydown);
}, },
beforeDestroy() { beforeDestroy() {
this.quill = null; if (this.quill) {
this.quill = null;
}
//
document.removeEventListener("keydown", this.handleGlobalKeydown);
}, },
methods: { methods: {
// Quill // Quill
initQuill() { initQuill() {
const editorDom = this.$refs.quillEditor; const editorDom = this.$refs.quillEditor;
if (!editorDom) {
console.warn("Quill container not found, retrying...");
setTimeout(() => {
this.initQuill();
}, 100);
return;
}
//
if (this.quill) {
return;
}
this.quill = new Quill(editorDom, { this.quill = new Quill(editorDom, {
theme: "snow", theme: "snow",
placeholder: "请输入消息...", placeholder: "请输入消息...",
@ -217,17 +238,26 @@ export default {
this.currentRange = 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("paste", this.handlePaste, true);
// //
this.quill.root.addEventListener("keydown", (e) => { this.quill.root.addEventListener("keydown", (e) => {
if (this.sendMode === "enter") { if (this.sendMode === "enter") {
if (e.key === "Enter" && !e.ctrlKey && !e.shiftKey) { // EnterEnterCtrl+Enter
if (e.key === "Enter" && !e.ctrlKey && !e.metaKey) {
e.preventDefault(); e.preventDefault();
this.handleSendText(); this.handleSendText();
} }
} else { } else {
// Ctrl+EnterCtrl+EnterEnter
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
this.handleSendText(); this.handleSendText();
@ -235,6 +265,59 @@ export default {
} }
}); });
}, },
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;
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);
}
},
clearEditor() { clearEditor() {
if (this.quill) { if (this.quill) {
@ -258,36 +341,79 @@ export default {
}, },
// //
handleScreenshotClick() { async handleScreenshotClick() {
this.$emit("open-screenshot"); 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("截图已插入到编辑器");
},
() => {
this.$message.info("已取消截图");
}
);
} catch (e) {
this.$message.error("截图失败: " + e.message);
}
}, },
// ========== @ ========== // ========== @ ==========
handleAtSelect(user) { handleAtSelect(user) {
console.log(user);
if (!this.quill || !user) return; if (!this.quill || !user) return;
//
const index = this.currentRange const index = this.currentRange
? this.currentRange.index ? this.currentRange.index
: this.quill.getLength(); : this.quill.getLength();
// @ +
const insertText = `@${user.name}`; const insertText = `@${user.name}`;
//
this.quill.insertText(index, insertText); this.quill.insertText(index, insertText);
// this.quill.formatText(index, insertText.length, {
color: "#409eff",
fontWeight: "500",
});
this.quill.setSelection(index + insertText.length); this.quill.setSelection(index + insertText.length);
// @
const hasUser = this.selectedAtUsers.find((u) => u.id === user.id); const hasUser = this.selectedAtUsers.find((u) => u.id === user.id);
if (!hasUser) { if (!hasUser) {
this.selectedAtUsers.push(user); this.selectedAtUsers.push(user);
} }
}, },
handleTeachingClick() { handleTeachingClick(meetingType) {
this.$emit("open-teaching"); 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),
},
});
}, },
generateRoomId(meetingType, id) {
handleConsultClick() { // 1. id30
this.$emit("open-consult"); const idStr = String(id).padStart(3, "0");
// = + 3id +
const roomId = meetingType + idStr + this.config.MEETING_PREFIX;
return roomId;
}, },
// @ID使 // @ID使
@ -305,21 +431,65 @@ export default {
async handleSendText() { async handleSendText() {
if (!this.quill || !this.currentChat) return; if (!this.quill || !this.currentChat) return;
const html = this.quill.root.innerHTML; const html = this.quill.root.innerHTML;
const text = this.quill.getText().trim(); const textContent = this.quill.getText().trim();
if (!text) return;
if (!textContent) return;
this.sending = true; this.sending = true;
const messageId = this.generateMessageId();
const timestamp = Date.now();
try { try {
const atUserIds = this.extractAtUsers(html); 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 });
}
const message = { 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 images = this.extractImagesFromEditor();
if (textOnly) {
messages.push({
at_users: atUserIds, at_users: atUserIds,
message_id: 0, message_id: 0,
payload: { payload: {
content: text, content: textContent,
file_duration: 0, file_duration: 0,
file_ico: "", file_ico: "",
file_name: "", file_name: "",
@ -332,41 +502,56 @@ export default {
target_id: this.currentChat.id, target_id: this.currentChat.id,
timestamp: 0, timestamp: 0,
type: "text", type: "text",
}; });
}
const body = { images.forEach((image) => {
client_id: this.getClientId(), messages.push({
message, 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",
},
scene: this.currentChat.scene,
source_id: this.userInfo.id,
target_id: this.currentChat.id, target_id: this.currentChat.id,
topic: this.getTopic(), timestamp: 0,
}; type: "image",
this.$emit("send-message", {
...message,
is_self: true,
sending: true,
message_id: messageId,
}); });
});
this.clearEditor(); return messages;
this.selectedAtUsers = []; },
if (this.currentChat.scene === ContactsScene.PRIVATE) { //
await sendPrivateMessage(body); extractImagesFromEditor() {
} else if (this.currentChat.scene === ContactsScene.GROUP) { const images = [];
await sendMultiChatMessage(body); if (!this.quill) return images;
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,
});
} }
});
this.$emit("update-message-status", messageId, { sending: false }); return images;
} catch (e) {
this.$message.error("发送失败: " + (e.message || "未知错误"));
this.$emit("update-message-status", messageId, {
sending: false,
sendFailed: true,
});
} finally {
this.sending = false;
}
}, },
// //
@ -376,11 +561,43 @@ export default {
const file = files[0]; const file = files[0];
e.target.value = ""; e.target.value = "";
if (!file.type.startsWith("image/")) {
this.$message.warning("只能上传图片文件");
return;
}
if (file.size > this.maxFileSize) { if (file.size > this.maxFileSize) {
this.$message.warning("文件大小不能超过100MB"); this.$message.warning("文件大小不能超过100MB");
return; return;
} }
await this.sendFileMessage(file, type);
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;
this.insertImageToEditor(uploadedPath);
this.$message.success("图片上传成功");
} catch (e) {
this.$message.error("图片上传失败: " + e.message);
}
}, },
async handlePaste(e) { async handlePaste(e) {
@ -402,7 +619,8 @@ export default {
async sendFileMessage(file, type) { async sendFileMessage(file, type) {
if (!this.currentChat) return; if (!this.currentChat) return;
const messageId = this.generateMessageId(); const messageId = this.generateMessageId();
const fileName = this.generateFileName(file.name); console.log("sendFileMessage", file);
const fileName = file.name;
const objectName = this.getObjectPath(fileName); const objectName = this.getObjectPath(fileName);
const fileType = const fileType =
@ -498,6 +716,26 @@ export default {
} }
}, },
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);
},
ensureMinioInitialized() {
const config = this.$store.getters.config;
if (!config || !config.MINIO_ENDPOINT) {
return this.$store.dispatch("GetNetConfig");
}
return Promise.resolve();
},
async uploadToMinIO(file, objectName) { async uploadToMinIO(file, objectName) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
@ -536,19 +774,25 @@ export default {
); );
}, },
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) { getObjectPath(fileName) {
const prefix = let sceneDir;
this.currentChat.scene === ContactsScene.GROUP switch (this.currentChat.scene) {
? MinioPaths.FILE_MULTICHAT case ContactsScene.GROUP:
: MinioPaths.FILE_PRIVATE; sceneDir = MinioPaths.FILE_MULTICHAT;
return prefix + fileName; 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}`;
}, },
getTopic() { getTopic() {
@ -586,100 +830,52 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.message-editor { .message-editor {
background: #fff; background: #fff;
border-top: 1px solid #ebeef5;
padding: 10px 16px; padding: 10px 16px;
position: relative; .quill-wrap {
} display: flex;
flex-direction: column;
.editor-toolbar { border: 1px solid #e4e7ed;
display: flex; border-radius: 4px;
align-items: center; gap: 8px;
gap: 4px;
margin-bottom: 8px; .quill-container {
height: 100px;
min-height: 100px;
overflow-y: auto;
box-sizing: border-box;
::v-deep .ql-container {
height: 100%;
font-size: 14px;
}
.el-button { ::v-deep .ql-editor {
font-size: 18px; min-height: 100%;
padding: 4px 8px; padding: 8px;
color: #606266; }
&:hover { ::v-deep .ql-toolbar {
color: #409eff; display: none;
}
}
.editor-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
} }
}
}
.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 { .editor-toolbar-left,
padding: 8px 12px; .editor-toolbar-right {
cursor: pointer; display: flex;
font-size: 14px; align-items: center;
gap: 12px;
&:hover { .el-button {
background: #f5f7fa; margin: 0;
padding: 4px 8px;
}
} }
} }
.at-panel-empty {
padding: 12px;
text-align: center;
font-size: 13px;
color: #c0c4cc;
}
} }
</style> </style>

@ -16,6 +16,7 @@
@send-message="handleSendMessage" @send-message="handleSendMessage"
@update-message-status="handleUpdateMessageStatus" @update-message-status="handleUpdateMessageStatus"
@font-size-change="handleFontSizeChange" @font-size-change="handleFontSizeChange"
@refresh-list="handleRefreshList"
/> />
</div> </div>
@ -25,7 +26,7 @@
ref="groupSettingRef" ref="groupSettingRef"
:chat="currentChat" :chat="currentChat"
:group-info="groupInfo" :group-info="groupInfo"
@quit-group="handleQuitGroup" @quit-group="handleDismissGroup"
@dismiss-group="handleDismissGroup" @dismiss-group="handleDismissGroup"
@refresh-list="handleRefreshList" @refresh-list="handleRefreshList"
/> />
@ -286,7 +287,6 @@ export default {
this.$refs.messageList.removeContact(multiChatId, ContactsScene.GROUP); this.$refs.messageList.removeContact(multiChatId, ContactsScene.GROUP);
} }
}, },
// //
handleRefreshList() { handleRefreshList() {
if (this.$refs.messageList) { if (this.$refs.messageList) {
@ -325,9 +325,6 @@ export default {
}, },
handleSendMessage(message) { handleSendMessage(message) {
if (this.$refs.messageDisplay) {
this.$refs.messageDisplay.addChatMessage(message);
}
// //
if (this.$refs.messageList) { if (this.$refs.messageList) {
this.$refs.messageList.updateContactLastMessage( this.$refs.messageList.updateContactLastMessage(
@ -337,6 +334,11 @@ export default {
new Date().toISOString() new Date().toISOString()
); );
} }
if (this.$refs.messageDisplay) {
this.$refs.messageDisplay.addChatMessage(message);
}
//
this.$refs.messageList.refreshList({ selectFirst: true });
}, },
handleUpdateMessageStatus(messageId, updates) { handleUpdateMessageStatus(messageId, updates) {
@ -369,15 +371,6 @@ export default {
handleGroupSetting() { handleGroupSetting() {
this.$refs.groupSettingRef.loadGroupInfo(); this.$refs.groupSettingRef.loadGroupInfo();
}, },
handleQuitGroup(chat) {
// 退
if (this.$refs.messageList) {
//
this.$refs.messageList.refreshList({ selectFirst: true });
}
},
handleDismissGroup(chat) { handleDismissGroup(chat) {
// //
if (this.$refs.messageList) { if (this.$refs.messageList) {

Loading…
Cancel
Save