2026-07-11 23:13:10 +08:00
|
|
|
|
%% 坐席端截图+拍照功能 — 时序图
|
|
|
|
|
|
%% 文件: docs/03-技术架构/sequence-diagram.mermaid
|
|
|
|
|
|
%% 包含 4 个关键流程:截图、拍照、图文发送、初始化与资源清理
|
|
|
|
|
|
|
|
|
|
|
|
%% ============================================
|
|
|
|
|
|
%% 流程 1:截图完整流程
|
|
|
|
|
|
%% ============================================
|
2026-07-09 11:50:19 +08:00
|
|
|
|
sequenceDiagram
|
2026-07-11 23:13:10 +08:00
|
|
|
|
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
|
2026-07-09 11:50:19 +08:00
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
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: 显示待发送图片预览
|
2026-07-09 11:50:19 +08:00
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
%% ============================================
|
|
|
|
|
|
%% 流程 2:拍照完整流程
|
|
|
|
|
|
%% ============================================
|
|
|
|
|
|
sequenceDiagram
|
|
|
|
|
|
participant User as 用户
|
|
|
|
|
|
participant RB as ReplyBox.vue
|
|
|
|
|
|
participant CC as CameraCapture.vue
|
|
|
|
|
|
participant UC as useCamera
|
|
|
|
|
|
participant UPI as usePendingImages
|
2026-07-09 11:50:19 +08:00
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
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)
|
2026-07-09 11:50:19 +08:00
|
|
|
|
end
|
2026-07-11 23:13:10 +08:00
|
|
|
|
|
|
|
|
|
|
RB->>RB: showCameraDialog = false
|
|
|
|
|
|
RB->>UPI: addImage(blob, 'camera')
|
|
|
|
|
|
UPI->>UPI: 生成 previewUrl,加入队列
|
|
|
|
|
|
UPI-->>RB: 队列已更新
|
|
|
|
|
|
RB-->>User: 输入区显示拍照缩略图
|
2026-07-09 11:50:19 +08:00
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
%% ============================================
|
|
|
|
|
|
%% 流程 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
|
2026-07-09 11:50:19 +08:00
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
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
|
2026-07-09 11:50:19 +08:00
|
|
|
|
end
|
2026-07-11 23:13:10 +08:00
|
|
|
|
|
|
|
|
|
|
opt 有文字
|
|
|
|
|
|
RB->>RB: emit('send', content)
|
|
|
|
|
|
RB->>RB: inputText = ''
|
2026-07-09 11:50:19 +08:00
|
|
|
|
end
|
2026-07-11 23:13:10 +08:00
|
|
|
|
|
|
|
|
|
|
RB->>RB: textareaHeight = 60
|
|
|
|
|
|
RB-->>User: 发送完成,输入区清空
|
2026-07-09 11:50:19 +08:00
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
%% ============================================
|
|
|
|
|
|
%% 流程 4:组件初始化与资源清理
|
|
|
|
|
|
%% ============================================
|
|
|
|
|
|
sequenceDiagram
|
|
|
|
|
|
participant RB as ReplyBox
|
|
|
|
|
|
participant SC as ScreenCapture
|
|
|
|
|
|
participant CC as CameraCapture
|
|
|
|
|
|
participant UPI as usePendingImages
|
2026-07-09 11:50:19 +08:00
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
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: 移除快捷键监听
|