diff --git a/src/layout/components/SystemSettingDialog.vue b/src/layout/components/SystemSettingDialog.vue index ebd6e43..76b2411 100644 --- a/src/layout/components/SystemSettingDialog.vue +++ b/src/layout/components/SystemSettingDialog.vue @@ -37,19 +37,21 @@
- 打开文件夹 - 修改 + 修改
+ +
- 打开文件夹 - 修改 + 修改
+ +
@@ -122,6 +124,7 @@ export default { visible(newVal) { if (newVal) { this.activeTab = "basic"; + this.refreshAuthStatus(); } }, // 监听基本设置变化,自动保存到本地 @@ -166,6 +169,8 @@ export default { return { visible: false, activeTab: "basic", + videoAuthorized: false, + cacheAuthorized: false, basicForm: { notification: true, sendKey: "enter", @@ -268,6 +273,84 @@ export default { // 用户取消操作 }); }, + // ==================== IndexedDB 目录句柄(与 realTimeConsultation 共享 HiUTalkStoreDB) ==================== + _openIDB() { + return new Promise((resolve, reject) => { + const req = indexedDB.open('HiUTalkStoreDB', 1); + req.onupgradeneeded = () => { req.result.createObjectStore('handles'); }; + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); + }, + async _getStoredDirHandle(key) { + const db = await this._openIDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction('handles', 'readonly'); + const req = tx.objectStore('handles').get(key); + req.onsuccess = () => { db.close(); resolve(req.result); }; + req.onerror = () => { db.close(); reject(req.error); }; + }); + }, + async _storeDirHandle(key, handle) { + const db = await this._openIDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction('handles', 'readwrite'); + const req = tx.objectStore('handles').put(handle, key); + req.onsuccess = () => { db.close(); resolve(); }; + req.onerror = () => { db.close(); reject(req.error); }; + }); + }, + // 刷新授权状态 + async refreshAuthStatus() { + this.videoAuthorized = await this._checkAuthorized('screenshot_dir'); + this.cacheAuthorized = await this._checkAuthorized('cache_dir'); + }, + async _checkAuthorized(key) { + const handle = await this._getStoredDirHandle(key); + if (!handle) return false; + const perm = await handle.queryPermission({ mode: 'readwrite' }); + return perm === 'granted'; + }, + // 视讯存储:修改保存文件夹 + async modifyVideoPath() { + await this._selectAndStorePath('screenshot_dir', 'videoPath', '视讯存储'); + }, + // 缓存存储:修改保存文件夹 + async modifyCachePath() { + await this._selectAndStorePath('cache_dir', 'cachePath', '缓存存储'); + }, + // 通用:弹出目录选择器 → 创建 HiUTalkStore 子文件夹 → 存储句柄 → 填充路径到输入框 + async _selectAndStorePath(handleKey, pathKey, label) { + if (!window.showDirectoryPicker) { + this.$message.error('当前浏览器不支持,请使用 Chrome 或 Edge'); + return; + } + try { + let dirHandle = await window.showDirectoryPicker({ mode: 'readwrite' }); + // 在选中目录下创建/获取 HiUTalkStore 子文件夹 + if (dirHandle.name !== 'HiUTalkStore') { + dirHandle = await dirHandle.getDirectoryHandle('HiUTalkStore', { create: true }); + } + await this._storeDirHandle(handleKey, dirHandle); + if (pathKey === 'videoPath') this.videoAuthorized = true; + if (pathKey === 'cachePath') this.cacheAuthorized = true; + // 提取当前路径的盘符(如 D:),拼接选中的文件夹名填充到输入框 + const drive = this._extractDrive(this.basicForm[pathKey]); + this.basicForm[pathKey] = `${drive}\\...\\${dirHandle.name}`; + this.$message.success(`${label}路径已更新`); + } catch (err) { + if (err.name !== 'AbortError') { + console.error(`${label}路径修改失败`, err); + this.$message.error('修改失败'); + } + } + }, + // 从路径中提取盘符,例如 "D:\RUS\HiUTalkStore" → "D:" + _extractDrive(path) { + if (!path) return 'D:'; + const match = path.match(/^([A-Za-z]:)/); + return match ? match[1] : 'D:'; + }, restoreDefault() { this.$confirm("确定要恢复默认设置吗?", "提示", { confirmButtonText: "确定", @@ -276,6 +359,8 @@ export default { }).then(() => { const version = this.$store.getters.loginInfo?.upgrade_data?.version || "V01.01.16"; const defaultPath = `D:\\RUS_${version}\\HiUTalkStore`; + this.videoAuthorized = false; + this.cacheAuthorized = false; this.basicForm = { notification: true, sendKey: "enter", @@ -335,4 +420,15 @@ export default { border-color: #ccc; color: #666; } +.auth-tag { + display: inline-block; + margin-top: 4px; + font-size: 12px; + &.authorized { + color: #67c23a; + } + &.unauthorized { + color: #e6a23c; + } +} diff --git a/src/views/videoCommunication/realTimeConsultation.vue b/src/views/videoCommunication/realTimeConsultation.vue index f1e26e5..ac58d90 100644 --- a/src/views/videoCommunication/realTimeConsultation.vue +++ b/src/views/videoCommunication/realTimeConsultation.vue @@ -458,7 +458,7 @@ export default { window.hirtcwebsdk.init({ serviceID: '56da5fd8921f4f7093a42e2a', serviceKey: '2c17c6393771ee3048ae34d6b965sdew', - Services: { BasicRoomServiceToken: "https://192.168.69.174:3001/v1/auth/token" }, + Services: { BasicRoomServiceToken: "https://wjw-ultrasoundappl/v1/auth/token" }, cameraLayers: [ { width: 320, @@ -902,18 +902,12 @@ export default { mimeType: 'video/webm;codecs=vp8,opus', }); this.recordedChunks = []; + const fileName = `${this.meetingTitle}录制_${new Date().toLocaleString().replace(/[/:\\s]/g, '_')}.webm`; this.mediaRecorder.ondataavailable = e => e.data.size && this.recordedChunks.push(e.data); - this.mediaRecorder.onstop = () => { + this.mediaRecorder.onstop = async () => { const blob = new Blob(this.recordedChunks, { type: 'video/webm' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `${this.meetingTitle}录制_${new Date().toLocaleString()}.webm`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - this.$message.success('录制完成'); + // 优先保存到系统设置的视讯存储路径,降级下载 + await this._saveBlobToVideoPath(blob, fileName, '录制'); }; this.mediaRecorder.start(); this.isRecording = true; @@ -921,6 +915,7 @@ export default { this.startRecordingTimer(); this.$message.warning('录制中'); } catch (e) { + console.error('录制失败', e); this.$message.error('录制失败'); } }, @@ -957,8 +952,44 @@ export default { this.saveMeetingSnapshot(); }, - // 保存会议界面截图 + // ==================== 截图保存辅助:IndexedDB 存储目录句柄 ==================== + _openIDB() { + return new Promise((resolve, reject) => { + const req = indexedDB.open('HiUTalkStoreDB', 1); + req.onupgradeneeded = () => { req.result.createObjectStore('handles'); }; + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); + }, + async _getStoredDirHandle() { + const db = await this._openIDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction('handles', 'readonly'); + const req = tx.objectStore('handles').get('screenshot_dir'); + req.onsuccess = () => { db.close(); resolve(req.result); }; + req.onerror = () => { db.close(); reject(req.error); }; + }); + }, + // 获取已授权的目录句柄(页面刷新后自动重授权,不弹目录选择器) + async _getAuthorizedHandle() { + const dirHandle = await this._getStoredDirHandle(); + if (!dirHandle) return null; + let perm = await dirHandle.queryPermission({ mode: 'readwrite' }); + // 页面刷新后权限变为 'prompt',需重新请求(saveMeetingSnapshot 由用户点击触发,具备手势上下文) + if (perm !== 'granted') { + try { + perm = await dirHandle.requestPermission({ mode: 'readwrite' }); + } catch (e) { + console.error('重新请求目录权限失败', e); + } + } + return perm === 'granted' ? dirHandle : null; + }, + + // 保存会议界面截图(保存到系统设置中的视讯存储路径,无弹窗) async saveMeetingSnapshot() { + // 1. 截图生成 canvas 和 blob + let blob, fileName; try { const html2canvas = (await import('html2canvas')).default; const container = document.querySelector('.meeting-container'); @@ -972,14 +1003,61 @@ export default { logging: false, allowTaint: false, }); - const link = document.createElement('a'); - link.download = `会诊截图_${new Date().toLocaleString()}.png`; - link.href = canvas.toDataURL('image/png'); - link.click(); - this.$message.success('截图已保存'); - } catch (err) { - console.error('截图失败', err); - this.$message.error('截图失败'); + fileName = `会诊截图_${new Date().toLocaleString().replace(/[/:\\s]/g, '_')}.png`; + blob = await new Promise((resolve, reject) => { + canvas.toBlob(b => { if (b) resolve(b); else reject(new Error('toBlob 失败')); }, 'image/png'); + }); + } catch (e) { + console.error('截图生成失败', e); + this.$message.error('截图生成失败'); + return; + } + + // 2. 优先保存到系统设置的视讯存储路径,降级下载 + await this._saveBlobToVideoPath(blob, fileName, '截图'); + }, + + // 将 blob 保存到系统设置的视讯存储路径(优先写入授权目录,降级下载) + async _saveBlobToVideoPath(blob, fileName, label) { + const videoPath = this._getSystemVideoPath(); + + // 1. 尝试写入已授权的目录 + const dirHandle = await this._getAuthorizedHandle(); + if (dirHandle) { + try { + const fileHandle = await dirHandle.getFileHandle(fileName, { create: true }); + const writable = await fileHandle.createWritable(); + await writable.write(blob); + await writable.close(); + this.$message.success(`${label}已保存到 ${videoPath || dirHandle.name}`); + return; + } catch (err) { + console.error(`${label}写入失败,降级为下载`, err); + } + } + + // 2. 降级方案:浏览器下载 + try { + const { saveAs } = await import('file-saver'); + saveAs(blob, fileName); + this.$message.success(videoPath + ? `${label}已下载(请在系统设置中授权"${videoPath}"以自动保存)` + : `${label}已保存`); + } catch (e) { + console.error(`${label}保存失败`, e); + this.$message.error(`${label}保存失败`); + } + }, + + // 从系统设置中读取视讯存储路径 + _getSystemVideoPath() { + try { + const raw = localStorage.getItem('systemSettings'); + if (!raw) return ''; + const settings = JSON.parse(raw); + return settings.basicForm?.videoPath || ''; + } catch (e) { + return ''; } },