From ed66c64bce789adc51675359694a89063cabc9d2 Mon Sep 17 00:00:00 2001 From: ysn <2126564605@qq.com> Date: Thu, 11 Jun 2026 14:12:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=B6=88=E6=81=AF=E6=A8=A1?= =?UTF-8?q?=E5=9D=97-=E5=8A=9F=E8=83=BD=E8=81=94=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/screenshot.js | 309 +++++--- .../message/components/MessageDisplay.vue | 29 +- .../message/components/MessageEditor.vue | 740 +++++++++++------- src/views/message/index.vue | 21 +- 4 files changed, 695 insertions(+), 404 deletions(-) diff --git a/src/utils/screenshot.js b/src/utils/screenshot.js index 3ce6f13..f2329e4 100644 --- a/src/utils/screenshot.js +++ b/src/utils/screenshot.js @@ -23,6 +23,9 @@ class ScreenshotTool { this.annotations = []; this.textInput = null; this.dpr = window.devicePixelRatio || 1; + + this.lineWidth = 2; + this.lineColor = '#ff4d4f'; } async start(onComplete, onCancel) { @@ -30,23 +33,23 @@ class ScreenshotTool { this.onCancel = onCancel; if (!this.isSupported()) { - throw new Error("当前浏览器不支持截图"); + throw new Error('当前浏览器不支持截图'); } try { this.stream = await navigator.mediaDevices.getDisplayMedia({ - video: { displaySurface: "monitor", width: 3840, height: 2160 }, + video: { displaySurface: 'monitor', width: 3840, height: 2160 }, audio: false, }); - this.video = document.createElement("video"); + this.video = document.createElement('video'); this.video.srcObject = this.stream; this.video.muted = true; await this.video.play(); this.createOverlay(); } catch (err) { - if (err.name === "NotAllowedError") throw new Error("已拒绝屏幕权限"); - throw new Error("截图启动失败"); + if (err.name === 'NotAllowedError') throw new Error('已拒绝屏幕权限'); + throw new Error('截图启动失败'); } } @@ -57,14 +60,14 @@ class ScreenshotTool { createOverlay() { this.scrollX = window.scrollX; this.scrollY = window.scrollY; - document.body.style.overflow = "hidden"; - document.documentElement.style.overflow = "hidden"; + document.body.style.overflow = 'hidden'; + document.documentElement.style.overflow = 'hidden'; - this.overlay = document.createElement("div"); - this.overlay.id = "screenshot-overlay"; + this.overlay = document.createElement('div'); + this.overlay.id = 'screenshot-overlay'; this.overlay.style.cssText = ` 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); @@ -77,78 +80,118 @@ class ScreenshotTool { const w = this.video.videoWidth; const h = this.video.videoHeight; - this.canvas = document.createElement("canvas"); + this.canvas = document.createElement('canvas'); this.canvas.width = w; 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.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.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.drawCanvas = document.createElement("canvas"); + this.drawCanvas = document.createElement('canvas'); this.drawCanvas.width = w; 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); } createToolbar() { - this.toolbar = document.createElement("div"); + this.toolbar = document.createElement('div'); this.toolbar.style.cssText = ` - position:fixed;bottom:60px;left:50%;transform:translateX(-50%); - background:#1f1f1f;border-radius:10px;padding:10px 20px; - display:flex;gap:28px;box-shadow:0 4px 20px rgba(0,0,0,0.4);z-index:1000000; + position:fixed;top:40px;left:50%;transform:translateX(-50%); + background:#fff;border-radius:8px;padding:6px; + display:flex;gap:4px;box-shadow:0 2px 12px rgba(0,0,0,0.15);z-index:1000000; `; - const btns = [ - { icon: "✓", color: "#67c23a", action: "confirm" }, - { icon: "✕", color: "#f56c6c", action: "cancel" }, - { icon: "🖵", color: "#409eff", action: "fullscreen" }, + const tools = [ + { icon: '▢', tool: 'rect', title: '矩形' }, + { icon: '○', tool: 'circle', title: '圆形' }, + { 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) => { - const el = document.createElement("button"); - el.innerHTML = `${b.icon}`; - el.style.cssText = `background:none;border:none;color:${b.color};cursor:pointer;padding:6px;border-radius:6px`; - el.onmouseover = () => el.style.background = "rgba(255,255,255,0.1)"; - el.onmouseout = () => el.style.background = "transparent"; - el.onclick = (e) => { e.stopPropagation(); this.handleAction(b.action); }; + tools.forEach((t) => { + const el = document.createElement('button'); + el.innerHTML = `${t.icon}`; + el.style.cssText = ` + background:none;border:none;color:#333;cursor:pointer; + padding:8px 12px;border-radius:6px; + 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.overlay.appendChild(this.toolbar); - this.actionToolbar = document.createElement("div"); + this.actionToolbar = document.createElement('div'); this.actionToolbar.style.cssText = ` - position:fixed;background:#fff;border-radius:8px;padding:6px 10px; - display:none;gap:12px;box-shadow:0 2px 12px rgba(0,0,0,0.15);z-index:1000001; + position:fixed;bottom:40px;left:50%;transform:translateX(-50%); + 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 = [ - { icon: "▢", tool: "rect" }, { icon: "○", tool: "circle" }, - { icon: "✏️", tool: "pen" }, { icon: "➤", tool: "arrow" }, - { icon: "A", tool: "text" }, { icon: "▦", tool: "mosaic" }, - { icon: "↩", tool: "undo" }, + const actions = [ + { icon: '✕', action: 'cancel', color: '#666', title: '取消' }, + { icon: '✓', action: 'confirm', color: '#52c41a', title: '确认' }, ]; - tools.forEach((t) => { - const el = document.createElement("button"); - el.innerHTML = `${t.icon}`; - el.style.cssText = "background:none;border:none;color:#333;cursor:pointer;padding:4px 6px;border-radius:4px"; - el.onmouseover = () => el.style.background = "#f0f0f0"; - el.onmouseout = () => el.style.background = "transparent"; - el.onclick = (e) => { e.stopPropagation(); this.handleAnnotateTool(t.tool); }; + actions.forEach((a) => { + const el = document.createElement('button'); + el.innerHTML = `${a.icon}`; + el.style.cssText = ` + background:none;border:none;color:${a.color};cursor:pointer; + padding:10px 20px;border-radius:6px; + 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.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() { - this.overlay.addEventListener("mousedown", (e) => { + this.overlay.addEventListener('mousedown', (e) => { if (this.toolbar.contains(e.target) || this.actionToolbar.contains(e.target)) return; if (this.isAnnotating && this.currentTool) { this.startAnnotation(e); @@ -161,7 +204,7 @@ class ScreenshotTool { } }); - this.overlay.addEventListener("mousemove", (e) => { + this.overlay.addEventListener('mousemove', (e) => { if (this.isDrawing) { this.currentX = e.clientX; this.currentY = e.clientY; @@ -171,65 +214,77 @@ class ScreenshotTool { } }); - this.overlay.addEventListener("mouseup", () => { + this.overlay.addEventListener('mouseup', () => { if (this.isDrawing) { 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) { this.finishAnnotation(); } }); - this.keydownHandler = (e) => e.key === "Escape" && this.cancel(); - document.addEventListener("keydown", this.keydownHandler); + this.keydownHandler = (e) => e.key === 'Escape' && this.cancel(); + document.addEventListener('keydown', this.keydownHandler); } updateSelection() { - const ctx = this.selectionCanvas.getContext("2d"); + const ctx = this.selectionCanvas.getContext('2d'); ctx.clearRect(0, 0, this.selectionCanvas.width, this.selectionCanvas.height); const x = Math.min(this.startX, this.currentX); const y = Math.min(this.startY, this.currentY); const w = Math.abs(this.currentX - this.startX); 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.clearRect(x, y, w, h); - ctx.strokeStyle = "#409eff"; - ctx.lineWidth = 2; - ctx.strokeRect(x, y, w, h); - } - showActionToolbar() { - const x = Math.min(this.startX, this.currentX); - const y = Math.min(this.startY, this.currentY); - const w = Math.abs(this.currentX - this.startX); - const h = Math.abs(this.currentY - this.startY); - if (w < 15 || h < 15) return; + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 1; + ctx.strokeRect(x, y, w, h); - this.isAnnotating = true; - this.drawCanvas.style.display = "block"; - this.actionToolbar.style.display = "flex"; - this.actionToolbar.style.left = `${x + w - 220}px`; - this.actionToolbar.style.top = `${y + h + 10}px`; + ctx.strokeStyle = '#52c41a'; + ctx.lineWidth = 2; + ctx.strokeRect(x + 1, y + 1, w - 2, h - 2); + + 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) { - if (tool === "undo") return this.undoAnnotation(); - this.currentTool = tool; - this.overlay.style.cursor = tool === "text" ? "text" : "crosshair"; + drawCorner(ctx, x, y, size) { + ctx.strokeStyle = '#52c41a'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(x - size, y); + ctx.lineTo(x, y); + ctx.lineTo(x, y - size); + ctx.stroke(); } startAnnotation(e) { this.isDrawingAnnotation = true; this.annotStartX = e.clientX; 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) { - if (!this.isDrawingAnnotation || !this.currentTool || this.currentTool === "text") return; - const ctx = this.drawCanvas.getContext("2d"); + if (!this.isDrawingAnnotation || !this.currentTool || this.currentTool === 'text') return; + const ctx = this.drawCanvas.getContext('2d'); ctx.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height); this.redrawAnnotations(ctx); @@ -237,61 +292,88 @@ class ScreenshotTool { const y = this.annotStartY; const ex = e.clientX; const ey = e.clientY; - ctx.strokeStyle = "#ff4d4f"; - ctx.fillStyle = "#ff4d4f"; - ctx.lineWidth = 2; + ctx.strokeStyle = this.lineColor; + ctx.fillStyle = this.lineColor; + ctx.lineWidth = this.lineWidth; switch (this.currentTool) { - case "rect": ctx.strokeRect(x, y, ex - x, ey - y); break; - case "circle": 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; + case 'rect': + ctx.strokeRect(x, y, ex - x, ey - y); + break; + case 'circle': + 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() { - if (!this.isDrawingAnnotation || !this.currentTool || this.currentTool === "text") return; + if (!this.isDrawingAnnotation || !this.currentTool || this.currentTool === 'text') return; 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)); } drawArrow(ctx, x1, y1, x2, y2) { - const head = 15; + const head = 12; 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.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.closePath(); ctx.fill(); + ctx.closePath(); + ctx.fill(); } drawMosaic(ctx, x, y) { const s = 8; - const data = ctx.getImageData(x - s / 2, y - s / 2, s, s); - const r = data.data[0], g = data.data[1], b = data.data[2]; + const rectX = Math.floor(x / s) * s; + 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.fillRect(x - s / 2, y - s / 2, s, s); + ctx.fillRect(rectX, rectY, s, s); } createTextInput(x, y) { if (this.textInput) this.textInput.remove(); - this.textInput = document.createElement("input"); + this.textInput = document.createElement('input'); this.textInput.style.cssText = ` 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; `; this.textInput.onkeydown = (e) => { - if (e.key === "Enter") { + if (e.key === 'Enter') { this.addText(x, y, this.textInput.value); - this.textInput.remove(); this.textInput = null; + this.textInput.remove(); + this.textInput = null; } }; this.textInput.onblur = () => { 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.textInput.focus(); @@ -299,30 +381,33 @@ class ScreenshotTool { addText(x, y, text) { if (!text) return; - const ctx = this.drawCanvas.getContext("2d"); - ctx.font = "16px Arial"; - ctx.fillStyle = "#ff4d4f"; + const ctx = this.drawCanvas.getContext('2d'); + ctx.font = '16px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; + ctx.fillStyle = '#ff4d4f'; ctx.fillText(text, x, y + 16); this.annotations.push(ctx.getImageData(0, 0, this.drawCanvas.width, this.drawCanvas.height)); } redrawAnnotations(ctx) { - this.annotations.forEach(d => ctx.putImageData(d, 0, 0)); + this.annotations.forEach((d) => ctx.putImageData(d, 0, 0)); } undoAnnotation() { if (this.annotations.length === 0) return; 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); this.redrawAnnotations(ctx); } handleAction(a) { switch (a) { - case "confirm": this.confirm(); break; - case "cancel": this.cancel(); break; - case "fullscreen": this.captureFullscreen(); break; + case 'confirm': + this.confirm(); + break; + case 'cancel': + this.cancel(); + break; } } @@ -339,10 +424,10 @@ class ScreenshotTool { } capture(cx, cy, cw, ch) { - const out = document.createElement("canvas"); + const out = document.createElement('canvas'); out.width = cw * this.dpr; out.height = ch * this.dpr; - const ctx = out.getContext("2d"); + const ctx = out.getContext('2d'); ctx.scale(this.dpr, this.dpr); 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); this.cleanup(); - out.toBlob(b => this.onComplete?.(b), "image/png", 1); + out.toBlob((b) => this.onComplete?.(b), 'image/png', 1); } cancel() { @@ -363,13 +448,13 @@ class ScreenshotTool { } cleanup() { - document.body.style.overflow = ""; - document.documentElement.style.overflow = ""; + document.body.style.overflow = ''; + document.documentElement.style.overflow = ''; window.scrollTo(this.scrollX, this.scrollY); this.overlay?.remove(); this.textInput?.remove(); - this.stream?.getTracks().forEach(t => t.stop()); - document.removeEventListener("keydown", this.keydownHandler); + this.stream?.getTracks().forEach((t) => t.stop()); + document.removeEventListener('keydown', this.keydownHandler); } } diff --git a/src/views/message/components/MessageDisplay.vue b/src/views/message/components/MessageDisplay.vue index 1a3f357..de06c64 100644 --- a/src/views/message/components/MessageDisplay.vue +++ b/src/views/message/components/MessageDisplay.vue @@ -5,7 +5,7 @@
{{ currentChat.name }} - ({{ currentChat.user_num }}) + ({{ currentChat.user_num }})
@@ -406,7 +406,8 @@ export default { newChat && (!oldChat || newChat.id !== oldChat.id || - newChat.scene !== oldChat.scene) + newChat.scene !== oldChat.scene || + newChat.last_message_id !== oldChat.last_message_id) ) { this.switchToChat(newChat); } @@ -535,15 +536,19 @@ export default { let merged; // 创建已存在消息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) { // 加载更新消息:追加到尾部 - 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]; } 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]; } @@ -658,7 +663,18 @@ export default { formatTextContent(content) { if (!content) return ""; + + // 如果 content 是对象,尝试获取其中的 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 text = text .replace(/&/g, "&") @@ -887,6 +903,7 @@ export default { this.fontSize = fontSize; const container = this.$refs.messageContainer; if (container) { + container.style.setProperty("--message-font-size", fontSize + "px"); container.style.fontSize = fontSize + "px"; } }, @@ -999,7 +1016,7 @@ export default { padding: 10px 14px; border-radius: 12px; word-break: break-word; - font-size: 14px; + font-size: var(--message-font-size, 14px); line-height: 1.5; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); diff --git a/src/views/message/components/MessageEditor.vue b/src/views/message/components/MessageEditor.vue index 9e0ac93..55d131d 100644 --- a/src/views/message/components/MessageEditor.vue +++ b/src/views/message/components/MessageEditor.vue @@ -1,130 +1,125 @@