import * as w6s from 'szient-js-sdk' import sha1 from 'crypto-js/sha1' import { siqCode } from '@/api/enterprise' import { baseUrl } from '@/config/env' import { $globalStore } from '@/common/globalStore' // ========== w6s 鉴权(参照 enterpriseHome.vue 的写法) ========== const generateRandomNumber = () => { const min = 100000 const max = 999999 const randomNum = (Math.floor(Math.random() * (max - min + 1)) + min) return randomNum.toString().padStart(6, '0') } let w6sAuthPromise = null // 确保 w6s 鉴权完成,缓存 Promise 避免重复鉴权 export const ensureW6sAuth = () => { if (w6sAuthPromise) return w6sAuthPromise w6sAuthPromise = new Promise((resolve, reject) => { siqCode(null, success => { let access_key = '' let access_secret = '' if (success.success) { access_key = success.data.accessKey access_secret = success.data.accessSecret } if (!access_key || !access_secret) { w6sAuthPromise = null reject(new Error('获取深i企认证信息错误: access_key 或 access_secret 为空')) return } const nonce = generateRandomNumber() const timestamp = `${Date.now()}` const str = [access_secret, nonce, timestamp] .sort() .reduce((s, item) => s + item, '') const signature = sha1(str).toString().toLowerCase() w6s.config({ access_key: access_key, nonce: nonce, timestamp: timestamp, signature: signature, }).then((res) => { console.log('w6s 鉴权成功', res) }).catch((err) => { w6sAuthPromise = null reject(err) }) w6s.ready(() => { console.log('w6s ready') resolve() }) }, error => { w6sAuthPromise = null reject(error) }) }) return w6sAuthPromise } // 格式化当前时间为 yyyy-MM-dd HH:mm:ss const formatDateTime = () => { const d = new Date() const pad = (n) => String(n).padStart(2, '0') return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}` } // 拍照并加水印(时间戳 + 经纬度自动叠加到照片) export const takePhotoWithWaterMark = async () => { // 如果调用方未传 content,则默认显示当前时间作为水印文字 const watermarkContent = formatDateTime(); await ensureW6sAuth(); console.log('[w6sMedia] 鉴权完成, 准备调起拍照') return new Promise((resolve, reject) => { w6s.image.takePhotoAndAddWaterMark({ content: watermarkContent, fontSize: 14, color: '#FF5858', markDisable: false, timeEnable: true, locationEnable: true, success: (res) => { console.log('[w6sMedia] takePhotoAndAddWaterMark success 原始返回:', res) console.log('[w6sMedia] 返回字段 imageURL:', res?.result.imageURL, 'key:', res?.result.key, 'imageInfo:', res?.result.imageInfo) resolve(res) }, fail: (err) => { console.error('[w6sMedia] takePhotoAndAddWaterMark fail:', err) reject(err) }, }) }) } // 将 MediaFile 从公共目录复制到应用私有目录(解决 Android 11+ 分区存储导致的 EACCES 权限问题) // 先通过 resolveLocalFileSystemURL(mediaFile.localURL) 拿到 FileEntry,再 copyTo dataDirectory // Android 11+ SAF 模式下 File plugin 能拿到 document URI 做 copyTo,FileTransfer native 层 open() 会失败 const copyMediaFileToAppDir = (mediaFile) => { return new Promise((resolve, reject) => { if (!window.cordova?.file?.dataDirectory) { reject(new Error('cordova.file.dataDirectory 不可用')) return } const sourceLocalURL = mediaFile.localURL || (mediaFile.toURL ? mediaFile.toURL() : '') console.log('[w6sMedia] copyMediaFileToAppDir 开始, localURL:', sourceLocalURL, 'fullPath:', mediaFile.fullPath) window.resolveLocalFileSystemURL( sourceLocalURL, (sourceFileEntry) => { console.log('[w6sMedia] 源 FileEntry 获取成功:', sourceFileEntry.fullPath) window.resolveLocalFileSystemURL( window.cordova.file.dataDirectory, (dataDirEntry) => { console.log('[w6sMedia] 应用私有目录:', dataDirEntry.fullPath) // 构造不冲突的目标文件名(加时间戳后缀) const baseName = mediaFile.name || 'video' const dotIdx = baseName.lastIndexOf('.') const safeName = dotIdx > 0 ? `${baseName.slice(0, dotIdx)}_${Date.now()}${baseName.slice(dotIdx)}` : `${baseName}_${Date.now()}` sourceFileEntry.copyTo( dataDirEntry, safeName, (newFileEntry) => { const fileUrl = newFileEntry.toURL() console.log('[w6sMedia] 文件已复制到应用私有目录, fileUrl:', fileUrl) resolve(fileUrl) }, (copyErr) => { console.error('[w6sMedia] copyTo 失败:', copyErr) reject(copyErr) } ) }, (dataDirErr) => { console.error('[w6sMedia] resolve dataDirectory 失败:', dataDirErr) reject(dataDirErr) } ) }, (sourceErr) => { console.error('[w6sMedia] resolve source localURL 失败:', sourceErr) reject(sourceErr) } ) }) } // 将 Cordova File API 的 File 对象读成 Blob // Cordova File 实现了 Blob 接口,可用 FileReader 读取 const cordovaFileToBlob = (cordovaFile) => { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => { const blob = new Blob([reader.result], { type: cordovaFile.type || 'application/octet-stream' }) resolve(blob) } reader.onerror = () => reject(reader.error) reader.readAsArrayBuffer(cordovaFile) }) } // 通过 fetch + FormData 上传 Blob(绕开 Cordova FileTransfer native 层 EACCES 问题) const uploadBlobViaFetch = async ({ blob, fileName, mimeType }) => { const token = $globalStore.useCommon?.token || '' const api = `${baseUrl}/app-enf/common/file/upload` const server = /^https?:/.test(api) ? api : `${window.location.origin}${api}` console.log('[w6sMedia] uploadBlobViaFetch 开始, server:', server, 'blob.size:', blob.size) const formData = new FormData() formData.append('file', blob, fileName) const resp = await fetch(server, { method: 'POST', headers: { Authorization: token ? `Bearer ${token}` : '', clientid: '9e6eb24cd4e169a98b0d2cd8034d877b', }, body: formData, }) const text = await resp.text() console.log('[w6sMedia] uploadBlobViaFetch response:', text) try { return JSON.parse(text) } catch { return text } } // 通过 resolveLocalFileSystemURL 获取 FileEntry → 读成 Blob → fetch 上传 // 用于视频等大文件,绕开 Cordova FileTransfer 在 Android 11+ 上的 EACCES 问题 const uploadMediaFileViaBlob = async ({ mediaFile, fileName, mimeType }) => { const localURL = mediaFile.localURL || (mediaFile.toURL ? mediaFile.toURL() : '') console.log('[w6sMedia] uploadMediaFileViaBlob 开始, localURL:', localURL) const fileEntry = await new Promise((resolve, reject) => { window.resolveLocalFileSystemURL(localURL, resolve, reject) }) const cordovaFile = await new Promise((resolve, reject) => { fileEntry.file(resolve, reject) }) console.log('[w6sMedia] 读取 Cordova File 成功, size:', cordovaFile.size, 'type:', cordovaFile.type) const blob = await cordovaFileToBlob(cordovaFile) return uploadBlobViaFetch({ blob, fileName, mimeType }) } // 通过 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] startVideoRecorderCordova 开始, 入参:', { duration, quality, front, syncSystemAlbum }) return new Promise((resolve, reject) => { if (!navigator?.device?.capture?.captureVideo) { console.error('[w6sMedia] navigator.device.capture.captureVideo 不可用') reject(new Error('当前环境不支持视频录制')) return } navigator.device.capture.captureVideo( async (mediaFiles) => { console.log('[w6sMedia] captureVideo success, mediaFiles:', mediaFiles) if (mediaFiles && mediaFiles.length > 0) { const file = mediaFiles[0] try { const appDirPath = await copyMediaFileToAppDir(file) console.log('[w6sMedia] 视频已复制到应用私有目录:', appDirPath) resolve({ video_path: appDirPath, name: file.name, size: file.size, type: file.type || 'video/mp4', _mediaFile: file, }) } catch (copyErr) { console.warn('[w6sMedia] 复制视频到应用私有目录失败,返回原始 MediaFile 作兜底:', copyErr) resolve({ video_path: file.localURL, name: file.name, size: file.size, type: file.type || 'video/mp4', _mediaFile: file, }) } } else { reject(new Error('录制未生成视频文件')) } }, (error) => { console.error('[w6sMedia] captureVideo fail:', error, 'code:', error?.code, 'message:', error?.message) reject(error) }, duration ? { limit: 1, duration } : { limit: 1 } ) }) } // 录制视频(对外统一入口) // 默认 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() return new Promise((resolve, reject) => { w6s.location.getLocation({ success: resolve, fail: reject }) }) } // 上传本地文件到后端 /app-enf/common/file/upload // 优先级: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, hasFile: !!file, hasMediaFile: !!mediaFile }) // 标准 File 对象(WebView 文件选择器返回,带 content URI 授权)直接 FormData 上传,无需鉴权 if (file) { return uploadBlobViaFetch({ blob: file, fileName, mimeType: mimeType || file.type }) } await ensureW6sAuth() // 优先用 FileTransfer(图片已验证能成功) try { return await uploadViaFileTransfer({ fileURL, fileName, mimeType }) } catch (err) { console.warn('[w6sMedia] FileTransfer 上传失败:', err?.exception || err?.message || err) // 如果有原始 MediaFile(视频场景),且错误是 EACCES 权限问题,自动 fallback fetch+Blob if (mediaFile) { const errMsg = String(err?.exception || err?.message || err || '') if (errMsg.includes('EACCES') || errMsg.includes('open failed') || err?.code === 1) { console.log('[w6sMedia] 检测到 EACCES 权限错误,fallback 到 fetch+Blob 方案') return uploadMediaFileViaBlob({ mediaFile, fileName, mimeType }) } } throw err } } // FileTransfer 实际实现 const uploadViaFileTransfer = ({ fileURL, fileName, mimeType }) => { return new Promise((resolve, reject) => { const token = $globalStore.useCommon?.token || '' const api = `${baseUrl}/app-enf/common/file/upload` const server = /^https?:/.test(api) ? api : `${window.location.origin}${api}` console.log('[w6sMedia] FileTransfer 上传, server:', server, 'fileURL:', fileURL) const opts = new window.FileUploadOptions() opts.fileKey = 'file' opts.fileName = fileName opts.mimeType = mimeType opts.headers = { Authorization: token ? `Bearer ${token}` : '', clientid: '9e6eb24cd4e169a98b0d2cd8034d877b', } w6s.file.upload( { fileURL, server, trustAllHosts: false, success: (res) => { console.log('[w6sMedia] FileTransfer success 原始返回:', res) try { const parsed = JSON.parse(res.response) resolve(parsed) } catch { resolve(res.response) } }, fail: (err) => { console.error('[w6sMedia] FileTransfer fail:', err) reject(err) }, }, opts ).catch((err) => { console.error('[w6sMedia] FileTransfer Promise reject:', err) reject(err) }) }) }