|
|
|
@ -1,4 +1,4 @@ |
|
|
|
import * as w6s from 'szient-js-sdk' |
|
|
|
import * as w6s from '@w6s/sdk' |
|
|
|
import sha1 from 'crypto-js/sha1' |
|
|
|
import { siqCode } from '@/api/enterprise' |
|
|
|
import { baseUrl } from '@/config/env' |
|
|
|
@ -65,22 +65,14 @@ export const ensureW6sAuth = () => { |
|
|
|
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())}` |
|
|
|
} |
|
|
|
|
|
|
|
// 拍照并加水印(时间戳 + 经纬度自动叠加到照片)
|
|
|
|
// 拍照并加水印(@w6s/sdk 2.x:timeEnable/locationEnable 时 SDK 自动在 content 后拼接当前时间与定位地址)
|
|
|
|
// 返回扁平结构 PhotoInfoAndMediaId: { imageURL, key, imageInfo, mediaId }
|
|
|
|
export const takePhotoWithWaterMark = async () => { |
|
|
|
// 如果调用方未传 content,则默认显示当前时间作为水印文字
|
|
|
|
const watermarkContent = formatDateTime(); |
|
|
|
await ensureW6sAuth(); |
|
|
|
await ensureW6sAuth() |
|
|
|
console.log('[w6sMedia] 鉴权完成, 准备调起拍照') |
|
|
|
return new Promise((resolve, reject) => { |
|
|
|
w6s.image.takePhotoAndAddWaterMark({ |
|
|
|
content: watermarkContent, |
|
|
|
content: '', |
|
|
|
fontSize: 14, |
|
|
|
color: '#FF5858', |
|
|
|
markDisable: false, |
|
|
|
@ -88,7 +80,7 @@ export const takePhotoWithWaterMark = async () => { |
|
|
|
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) |
|
|
|
console.log('[w6sMedia] 返回字段 imageURL:', res?.imageURL, 'key:', res?.key, 'imageInfo:', res?.imageInfo) |
|
|
|
resolve(res) |
|
|
|
}, |
|
|
|
fail: (err) => { |
|
|
|
@ -99,243 +91,49 @@ export const takePhotoWithWaterMark = async () => { |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
// 将 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 后,<input capture="camcorder"> 会直接调起摄像头录像(不弹相册/文件选择),
|
|
|
|
// 返回的 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, |
|
|
|
// 录制视频(w6s 官方 API,文档 https://open.beeworks.cn/js-sdk/video.html)
|
|
|
|
// 返回 info: { video_path 本地路径, video_duration 时长, video_size 大小(K), video_thumbnail 首帧 base64 }
|
|
|
|
export const startVideoRecorder = async ({ |
|
|
|
duration = 0, // 最大录制时长(秒),0 不限制
|
|
|
|
quality = 1, // 0 高清 / 1 一般 / 2 流畅
|
|
|
|
front = false, |
|
|
|
syncSystemAlbum = false, |
|
|
|
} = {}) => { |
|
|
|
console.log('[w6sMedia] startVideoRecorderCordova 开始, 入参:', { duration, quality, front, syncSystemAlbum }) |
|
|
|
console.log('[w6sMedia] startVideoRecorder 开始, 入参:', { duration, quality, front, syncSystemAlbum }) |
|
|
|
await ensureW6sAuth() |
|
|
|
console.log('[w6sMedia] 录制前鉴权完成, 准备调起录制') |
|
|
|
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 { |
|
|
|
w6s.video.startVideoRecoder({ |
|
|
|
duration, |
|
|
|
quality: String(quality), |
|
|
|
sync_system_album: syncSystemAlbum, |
|
|
|
front, |
|
|
|
success: (res) => { |
|
|
|
console.log('[w6sMedia] startVideoRecoder success 原始返回:', res) |
|
|
|
const info = res?.info || {} |
|
|
|
if (!info.video_path) { |
|
|
|
reject(new Error('录制未生成视频文件')) |
|
|
|
return |
|
|
|
} |
|
|
|
// 从路径提取文件名(可能带查询参数)
|
|
|
|
const name = (info.video_path.split('/').pop() || '').split('?')[0] || `video-${Date.now()}.mp4` |
|
|
|
resolve({ |
|
|
|
video_path: info.video_path, |
|
|
|
video_duration: info.video_duration, |
|
|
|
video_size: info.video_size, |
|
|
|
video_thumbnail: info.video_thumbnail, |
|
|
|
name, |
|
|
|
type: 'video/mp4', |
|
|
|
}) |
|
|
|
}, |
|
|
|
(error) => { |
|
|
|
console.error('[w6sMedia] captureVideo fail:', error, 'code:', error?.code, 'message:', error?.message) |
|
|
|
reject(error) |
|
|
|
fail: (err) => { |
|
|
|
console.error('[w6sMedia] startVideoRecoder fail:', err) |
|
|
|
reject(err) |
|
|
|
}, |
|
|
|
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() |
|
|
|
@ -344,81 +142,40 @@ export const getLocation = async () => { |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
// 上传本地文件到后端 /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 |
|
|
|
} |
|
|
|
// 上传本地文件到后端 /app-enf/common/file/upload(Cordova FileTransfer,图片/视频共用)
|
|
|
|
export const uploadLocalFileToBackend = ({ fileURL, fileName = 'upload', mimeType = 'image/jpeg' }) => { |
|
|
|
console.log('[w6sMedia] uploadLocalFileToBackend 开始, 入参:', { fileURL, fileName, mimeType }) |
|
|
|
return ensureW6sAuth().then(() => uploadViaFileTransfer({ fileURL, fileName, mimeType })) |
|
|
|
} |
|
|
|
|
|
|
|
// 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) |
|
|
|
// FileTransfer 实际实现(@w6s/sdk 2.x 的类式 API:new FileUpload(options) → .upload(params))
|
|
|
|
const uploadViaFileTransfer = async ({ fileURL, 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] FileTransfer 上传, server:', server, 'fileURL:', fileURL) |
|
|
|
|
|
|
|
const opts = new window.FileUploadOptions() |
|
|
|
opts.fileKey = 'file' |
|
|
|
opts.fileName = fileName |
|
|
|
opts.mimeType = mimeType |
|
|
|
opts.headers = { |
|
|
|
const opts = { |
|
|
|
fileKey: 'file', |
|
|
|
fileName, |
|
|
|
mimeType, |
|
|
|
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) |
|
|
|
}) |
|
|
|
const uploader = new w6s.file.upload(opts) |
|
|
|
// upload 返回 Promise:responseCode === 200 时 resolve,否则 reject
|
|
|
|
const res = await uploader.upload({ |
|
|
|
fileURL, |
|
|
|
server, |
|
|
|
trustAllHosts: false, |
|
|
|
}) |
|
|
|
console.log('[w6sMedia] FileTransfer success 原始返回:', res) |
|
|
|
try { |
|
|
|
return JSON.parse(res.response) |
|
|
|
} catch { |
|
|
|
return res.response |
|
|
|
} |
|
|
|
} |
|
|
|
|