%% 坐席端截图+拍照功能 — 时序图 %% 文件: docs/03-技术架构/sequence-diagram.mermaid %% 包含 4 个关键流程:截图、拍照、图文发送、初始化与资源清理 %% ============================================ %% 流程 1:截图完整流程 %% ============================================ sequenceDiagram participant User as 用户 participant RB as ReplyBox.vue participant USC as useScreenCapture participant SC as ScreenCapture.vue participant UPI as usePendingImages participant PIP as PendingImagePreview.vue User->>RB: 点击截图按钮 RB->>RB: handleScreenshot() RB->>USC: captureScreenAsCanvas() Note over USC: 浏览器弹出 getDisplayMedia 选择器 User->>USC: 选择屏幕/窗口/标签页 USC->>USC: getDisplayMedia() 获取视频流 USC->>USC: video.play() → canvas.drawImage() 捕获一帧 USC->>USC: stream.getTracks().stop() 停止共享 USC-->>RB: 返回 HTMLCanvasElement RB->>RB: screenshotCanvas = canvas RB->>RB: showScreenCapture = true Note over SC: 组件挂载,显示全屏遮罩 SC->>SC: 初始化:底图 Canvas + 遮罩 Canvas SC->>SC: drawMask() 绘制半透明遮罩 User->>SC: 鼠标拖拽框选区域 SC->>SC: 更新 box {x, y, w, h} SC->>SC: drawMask() 清除选区内遮罩 User->>SC: 选择马赛克工具 User->>SC: 在选区内拖拽涂抹 SC->>SC: saveHistory() 保存当前状态 SC->>SC: drawMosaic() 像素化处理 User->>SC: 选择箭头工具 User->>SC: 拖拽绘制箭头 SC->>SC: saveHistory() 保存当前状态 SC->>SC: drawArrow() 绘制箭头线段 User->>SC: 点击撤销 SC->>SC: historyIndex-- SC->>SC: ctx.putImageData() 恢复上一状态 User->>SC: 点击确认 ✓ SC->>SC: 从编辑 Canvas 裁剪选区 SC->>SC: canvas.toBlob() 转 Blob SC-->>RB: emit('confirm', blob) RB->>RB: showScreenCapture = false RB->>UPI: addImage(blob, 'screenshot') UPI->>UPI: URL.createObjectURL(blob) 生成预览URL UPI->>UPI: images.push({ id, blob, previewUrl, source }) UPI-->>RB: 队列已更新 RB->>PIP: 响应式传递 images PIP->>PIP: 渲染缩略图 + × 删除按钮 PIP-->>User: 显示待发送图片预览 %% ============================================ %% 流程 2:拍照完整流程 %% ============================================ sequenceDiagram participant User as 用户 participant RB as ReplyBox.vue participant CC as CameraCapture.vue participant UC as useCamera participant UPI as usePendingImages User->>RB: 点击拍照按钮 RB->>RB: handleCamera() RB->>RB: showCameraDialog = true RB->>CC: 挂载组件(v-model=true) CC->>CC: ElMessage.info('即将请求摄像头权限...') CC->>UC: startCamera() UC->>UC: getUserMedia({ video: { facingMode: 'user' } }) Note over UC: 浏览器弹出权限请求 User->>UC: 允许摄像头访问 UC->>UC: stream 赋值到 video.srcObject UC->>UC: video.play() UC-->>CC: 摄像头预览已启动 CC-->>User: 显示实时摄像头预览 User->>CC: 点击拍照按钮 CC->>UC: captureFrame() UC->>UC: canvas.drawImage(video, 0, 0) UC->>UC: canvas.toDataURL('image/png') UC-->>CC: 返回图片 dataURL CC->>CC: capturedImage = dataURL CC->>CC: 隐藏 video,显示 img 静态画面 CC-->>User: 显示拍照结果 + 重拍/确认按钮 alt 用户点击重拍 User->>CC: 点击重拍 CC->>CC: capturedImage = null CC->>CC: 恢复 video 实时预览 CC-->>User: 回到实时预览状态 else 用户点击确认 User->>CC: 点击确认 ✓ CC->>UC: captureFrame() CC->>CC: dataURL → Blob 转换 CC->>UC: stopCamera() 停止摄像头 CC-->>RB: emit('confirm', blob) CC-->>RB: emit('update:modelValue', false) end RB->>RB: showCameraDialog = false RB->>UPI: addImage(blob, 'camera') UPI->>UPI: 生成 previewUrl,加入队列 UPI-->>RB: 队列已更新 RB-->>User: 输入区显示拍照缩略图 %% ============================================ %% 流程 3:图文组合发送流程 %% ============================================ sequenceDiagram participant User as 用户 participant RB as ReplyBox.vue participant UPI as usePendingImages participant API_U as uploadFile() participant API_M as sendMessage() participant Store as conversationStore User->>RB: 输入文字 + 有待发送图片 User->>RB: 点击发送 / 按 Enter RB->>RB: handleSend() RB->>RB: content = inputText.trim() RB->>RB: hasImages = pendingImages.count > 0 RB->>RB: hasText = content.length > 0 alt 无图片且无文字 RB->>RB: return(不发送) else 有图片 RB->>RB: ElMessage.info('发送中...') RB->>UPI: 获取 images 数组 loop 遍历每张图片 RB->>API_U: uploadFile(image.blob) API_U-->>RB: { url, filename, file_size } RB->>API_M: sendMessage(convId, '[图片]', 'image', { media_url, file_name, file_size }) API_M-->>RB: 返回新消息 Message RB->>Store: messages.push(newMsg) end RB->>UPI: clearAll() 清空图片队列 RB->>RB: URL.revokeObjectURL() 释放预览URL end opt 有文字 RB->>RB: emit('send', content) RB->>RB: inputText = '' end RB->>RB: textareaHeight = 60 RB-->>User: 发送完成,输入区清空 %% ============================================ %% 流程 4:组件初始化与资源清理 %% ============================================ sequenceDiagram participant RB as ReplyBox participant SC as ScreenCapture participant CC as CameraCapture participant UPI as usePendingImages Note over RB: 组件挂载 onMounted RB->>RB: 注册全局快捷键 Note over RB: 用户触发截图 RB->>SC: v-if="showScreenCapture" 挂载 SC->>SC: onMounted → 初始化 Canvas、事件监听 SC->>SC: drawMask() 绘制初始遮罩 Note over RB: 截图取消/确认 RB->>SC: v-if=false 卸载 SC->>SC: onUnmounted → 移除事件监听、清空 history Note over RB: 用户触发拍照 RB->>CC: v-model=true 挂载 CC->>CC: onMounted → startCamera() Note over RB: 拍照取消/确认 RB->>CC: v-model=false 卸载 CC->>CC: onUnmounted → stopCamera() 停止流 Note over RB: ReplyBox 卸载 RB->>RB: onUnmounted RB->>UPI: clearAll() → revokeObjectURL 释放内存 RB->>RB: 移除快捷键监听