From 51b7675ae5d18651b8bea0745dedf8b137459d7a Mon Sep 17 00:00:00 2001 From: "wangmingyong@163.com" Date: Thu, 27 Aug 2026 16:40:36 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=86=E9=A2=91=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/util/w6sMedia.js | 99 ++++++++++++++++--- .../nos/components/NosDetailMaterialsTab.vue | 25 ++--- 2 files changed, 101 insertions(+), 23 deletions(-) diff --git a/zdxt-web-client/zdxt-efcode-enterprise-app/src/util/w6sMedia.js b/zdxt-web-client/zdxt-efcode-enterprise-app/src/util/w6sMedia.js index 746b8389..69e1fb2e 100644 --- a/zdxt-web-client/zdxt-efcode-enterprise-app/src/util/w6sMedia.js +++ b/zdxt-web-client/zdxt-efcode-enterprise-app/src/util/w6sMedia.js @@ -210,16 +210,78 @@ const uploadMediaFileViaBlob = async ({ mediaFile, fileName, mimeType }) => { return uploadBlobViaFetch({ blob, fileName, mimeType }) } -// 录制视频(使用 Cordova 原生 Media Capture API) -export const startVideoRecorder = async ({ +// 通过 WebView 文件选择器调起系统摄像头录像(推荐方式) +// 原理:Android WebView 实现 onShowFileChooser 后, 会直接调起摄像头录像(不弹相册/文件选择), +// 返回的 File 通过 content URI 授权,页面可直接读取/上传,彻底绕开 Cordova 在分区存储下的 EACCES 问题 +export const startVideoRecorderViaInput = () => { + return new Promise((resolve, reject) => { + const input = document.createElement('input') + input.type = 'file' + input.setAttribute('accept', 'video/*') + // capture 属性强制现场录制;camcorder 指定调起录像 + input.setAttribute('capture', 'camcorder') + input.style.display = 'none' + document.body.appendChild(input) + + let settled = false + const cleanup = () => { + window.removeEventListener('focus', onFocus) + input.remove() + } + const settleResolve = (payload) => { + if (settled) return + settled = true + cleanup() + resolve(payload) + } + const settleReject = (err) => { + if (settled) return + settled = true + cleanup() + reject(err) + } + + input.addEventListener('change', () => { + const file = input.files && input.files[0] + if (!file) { + settleReject(new Error('未录制视频')) + return + } + console.log('[w6sMedia] input 录像返回 File:', file.name, 'size:', file.size, 'type:', file.type) + settleResolve({ + video_path: '', + file, // 标准 File 对象,可直接 FormData 上传 + name: file.name, + size: file.size, + type: file.type || 'video/mp4', + }) + }) + // 用户取消录制时部分 WebView 会触发 cancel 事件 + input.addEventListener('cancel', () => settleReject(new Error('已取消录制'))) + // 兜底:摄像头关闭回到页面后若仍无文件,视为取消 + const onFocus = () => { + setTimeout(() => { + if (!input.files || input.files.length === 0) { + settleReject(new Error('已取消录制')) + } + }, 1500) + } + window.addEventListener('focus', onFocus) + + input.click() + }) +} + +// 录制视频(Cordova Media Capture 方式,备选) +// 注意:Android 10+ 分区存储下,公共目录文件的 FileTransfer/copyTo/FileReader 全部会 EACCES, +// 仅在设备允许旧版存储(legacy storage)时可用 +export const startVideoRecorderCordova = ({ duration = 0, quality = 1, front = false, syncSystemAlbum = false, } = {}) => { - console.log('[w6sMedia] startVideoRecorder 开始, 入参:', { duration, quality, front, syncSystemAlbum }) - await ensureW6sAuth() - console.log('[w6sMedia] 录制前鉴权完成, 准备调起录制') + console.log('[w6sMedia] startVideoRecorderCordova 开始, 入参:', { duration, quality, front, syncSystemAlbum }) return new Promise((resolve, reject) => { if (!navigator?.device?.capture?.captureVideo) { console.error('[w6sMedia] navigator.device.capture.captureVideo 不可用') @@ -231,9 +293,6 @@ export const startVideoRecorder = async ({ console.log('[w6sMedia] captureVideo success, mediaFiles:', mediaFiles) if (mediaFiles && mediaFiles.length > 0) { const file = mediaFiles[0] - console.log('[w6sMedia] 视频文件: fullPath=', file.fullPath, 'localURL=', file.localURL, 'name=', file.name, 'size=', file.size) - // 优先把文件复制到应用私有目录,后续上传时传复制后的路径 - // 如果复制失败,fallback 返回原始 localURL,让上传端再尝试 fetch+Blob 方案 try { const appDirPath = await copyMediaFileToAppDir(file) console.log('[w6sMedia] 视频已复制到应用私有目录:', appDirPath) @@ -242,7 +301,7 @@ export const startVideoRecorder = async ({ name: file.name, size: file.size, type: file.type || 'video/mp4', - _mediaFile: file, // 附带原始 MediaFile,供 fetch+Blob 上传兜底 + _mediaFile: file, }) } catch (copyErr) { console.warn('[w6sMedia] 复制视频到应用私有目录失败,返回原始 MediaFile 作兜底:', copyErr) @@ -267,6 +326,16 @@ export const startVideoRecorder = async ({ }) } +// 录制视频(对外统一入口) +// 默认 mode='input':WebView 文件选择器 + capture 强制现场录像,返回可读 File 对象(推荐) +// mode='cordova':Cordova MediaCapture + 复制私有目录(部分设备受分区存储限制不可用) +export const startVideoRecorder = ({ mode = 'input', duration = 0, quality = 1 } = {}) => { + if (mode === 'cordova') { + return startVideoRecorderCordova({ duration, quality }) + } + return startVideoRecorderViaInput() +} + // 获取定位(经纬度) export const getLocation = async () => { await ensureW6sAuth() @@ -276,15 +345,21 @@ export const getLocation = async () => { } // 上传本地文件到后端 /app-enf/common/file/upload -// 默认使用 w6s.file.upload(Cordova FileTransfer) -// 如果传入 mediaFile(视频场景),FileTransfer 失败(如 EACCES)时自动 fallback 到 fetch+Blob 方案 +// 优先级:file(WebView 文件选择器返回的标准 File,FormData 直传)> FileTransfer > fetch+Blob export const uploadLocalFileToBackend = async ({ fileURL, fileName = 'upload', mimeType = 'image/jpeg', + file = null, mediaFile = null, }) => { - console.log('[w6sMedia] uploadLocalFileToBackend 开始, 入参:', { fileURL, fileName, mimeType, hasMediaFile: !!mediaFile }) + console.log('[w6sMedia] uploadLocalFileToBackend 开始, 入参:', { fileURL, fileName, mimeType, hasFile: !!file, hasMediaFile: !!mediaFile }) + + // 标准 File 对象(WebView 文件选择器返回,带 content URI 授权)直接 FormData 上传,无需鉴权 + if (file) { + return uploadBlobViaFetch({ blob: file, fileName, mimeType: mimeType || file.type }) + } + await ensureW6sAuth() // 优先用 FileTransfer(图片已验证能成功) diff --git a/zdxt-web-client/zdxt-efcode-enterprise-app/src/views/enterprise/nos/components/NosDetailMaterialsTab.vue b/zdxt-web-client/zdxt-efcode-enterprise-app/src/views/enterprise/nos/components/NosDetailMaterialsTab.vue index 83cf3689..569fc9c8 100644 --- a/zdxt-web-client/zdxt-efcode-enterprise-app/src/views/enterprise/nos/components/NosDetailMaterialsTab.vue +++ b/zdxt-web-client/zdxt-efcode-enterprise-app/src/views/enterprise/nos/components/NosDetailMaterialsTab.vue @@ -457,14 +457,16 @@ const onTakePhoto = async (item) => { const onRecordVideo = async (item) => { console.log('[NosDetail] onRecordVideo 开始, item:', item) try { - const res = await startVideoRecorder({ duration: 0, quality: 1 }) - console.log('[NosDetail] 录制返回 res:', res, 'video_path:', res?.video_path) + // 默认 input 方式:WebView 文件选择器 + capture 强制现场录像,返回可直接上传的 File 对象 + const res = await startVideoRecorder() + console.log('[NosDetail] 录制返回 res:', res, 'file:', res?.file, 'video_path:', res?.video_path) await pushSdkFileToFileList(item, { - localPath: res.video_path, - fileName: `video-${Date.now()}.mp4`, + localPath: res.video_path || '', + file: res.file || null, + fileName: res.file && /\.mp4$/i.test(res.file.name) ? res.file.name : `video-${Date.now()}.mp4`, mimeType: res.type || 'video/mp4', isImage: false, - mediaFile: res._mediaFile, + mediaFile: res._mediaFile || null, }) } catch (err) { console.error('[NosDetail] w6s 录制失败:', err) @@ -473,11 +475,12 @@ const onRecordVideo = async (item) => { } // 通用:把 SDK 返回的本地文件上传后端 → 写入 fileList -// mediaFile 可选:视频场景传入原始 MediaFile,FileTransfer 失败时 fallback fetch+Blob -const pushSdkFileToFileList = async (item, { localPath, fileName, mimeType, isImage, mediaFile = null }) => { - console.log('[NosDetail] pushSdkFileToFileList 开始, 入参:', { localPath, fileName, mimeType, isImage, hasMediaFile: !!mediaFile }) - if (!localPath) { - console.warn('[NosDetail] pushSdkFileToFileList localPath 为空, 直接返回') +// file:WebView 文件选择器返回的标准 File(视频 input 方式),直接 FormData 上传 +// mediaFile:Cordova MediaFile(视频 cordova 方式),FileTransfer 失败时 fallback fetch+Blob +const pushSdkFileToFileList = async (item, { localPath, fileName, mimeType, isImage, file = null, mediaFile = null }) => { + console.log('[NosDetail] pushSdkFileToFileList 开始, 入参:', { localPath, fileName, mimeType, isImage, hasFile: !!file, hasMediaFile: !!mediaFile }) + if (!localPath && !file) { + console.warn('[NosDetail] pushSdkFileToFileList localPath 和 file 均为空, 直接返回') return } const fileObj = { @@ -490,7 +493,7 @@ const pushSdkFileToFileList = async (item, { localPath, fileName, mimeType, isIm const reactiveFile = item.fileList[item.fileList.length - 1] console.log('[NosDetail] 已 push 到 fileList, 当前 fileList 长度:', item.fileList.length) try { - const res = await uploadLocalFileToBackend({ fileURL: localPath, fileName, mimeType, mediaFile }) + const res = await uploadLocalFileToBackend({ fileURL: localPath, fileName, mimeType, file, mediaFile }) console.log('[NosDetail] 上传后端返回 res:', res) const data = res && res.data ? res.data : res const filePath = typeof data === 'string'