|
|
@ -100,80 +100,120 @@ export const takePhotoWithWaterMark = async () => { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 将 MediaFile 从公共目录复制到应用私有目录(解决 Android 11+ 分区存储导致的 EACCES 权限问题)
|
|
|
// 将 MediaFile 从公共目录复制到应用私有目录(解决 Android 11+ 分区存储导致的 EACCES 权限问题)
|
|
|
// 直接使用 MediaFile.file() 获取 FileEntry,避免 resolveLocalFileSystemURL 在公共目录路径上的限制
|
|
|
// 先通过 resolveLocalFileSystemURL(mediaFile.localURL) 拿到 FileEntry,再 copyTo dataDirectory
|
|
|
|
|
|
// Android 11+ SAF 模式下 File plugin 能拿到 document URI 做 copyTo,FileTransfer native 层 open() 会失败
|
|
|
const copyMediaFileToAppDir = (mediaFile) => { |
|
|
const copyMediaFileToAppDir = (mediaFile) => { |
|
|
return new Promise((resolve, reject) => { |
|
|
return new Promise((resolve, reject) => { |
|
|
if (!window.cordova?.file?.dataDirectory) { |
|
|
if (!window.cordova?.file?.dataDirectory) { |
|
|
reject(new Error('cordova.file.dataDirectory 不可用')) |
|
|
reject(new Error('cordova.file.dataDirectory 不可用')) |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
console.log('[w6sMedia] copyMediaFileToAppDir 开始, localURL:', mediaFile.localURL, 'fullPath:', mediaFile.fullPath) |
|
|
const sourceLocalURL = mediaFile.localURL || (mediaFile.toURL ? mediaFile.toURL() : '') |
|
|
|
|
|
console.log('[w6sMedia] copyMediaFileToAppDir 开始, localURL:', sourceLocalURL, 'fullPath:', mediaFile.fullPath) |
|
|
|
|
|
|
|
|
// 通过 MediaFile.file() 获取 FileEntry(比 resolveLocalFileSystemURL 更可靠)
|
|
|
window.resolveLocalFileSystemURL( |
|
|
mediaFile.file( |
|
|
sourceLocalURL, |
|
|
(sourceFileEntry) => { |
|
|
(sourceFileEntry) => { |
|
|
console.log('[w6sMedia] 获取源 FileEntry 成功:', sourceFileEntry.fullPath) |
|
|
console.log('[w6sMedia] 源 FileEntry 获取成功:', sourceFileEntry.fullPath) |
|
|
window.resolveLocalFileSystemURL( |
|
|
window.resolveLocalFileSystemURL( |
|
|
window.cordova.file.dataDirectory, |
|
|
window.cordova.file.dataDirectory, |
|
|
(dataDirEntry) => { |
|
|
(dataDirEntry) => { |
|
|
console.log('[w6sMedia] 应用私有目录:', dataDirEntry.fullPath) |
|
|
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( |
|
|
sourceFileEntry.copyTo( |
|
|
dataDirEntry, |
|
|
dataDirEntry, |
|
|
mediaFile.name, |
|
|
safeName, |
|
|
(newFileEntry) => { |
|
|
(newFileEntry) => { |
|
|
const fileUrl = newFileEntry.toURL() |
|
|
const fileUrl = newFileEntry.toURL() |
|
|
console.log('[w6sMedia] 文件已复制到应用私有目录, fileUrl:', fileUrl) |
|
|
console.log('[w6sMedia] 文件已复制到应用私有目录, fileUrl:', fileUrl) |
|
|
resolve(fileUrl) |
|
|
resolve(fileUrl) |
|
|
}, |
|
|
}, |
|
|
(copyErr) => { |
|
|
(copyErr) => { |
|
|
// 目标文件已存在时,先删除再复制
|
|
|
console.error('[w6sMedia] copyTo 失败:', copyErr) |
|
|
console.warn('[w6sMedia] copyTo 可能目标已存在,尝试移除后重试:', copyErr) |
|
|
reject(copyErr) |
|
|
window.resolveLocalFileSystemURL( |
|
|
|
|
|
window.cordova.file.dataDirectory + mediaFile.name, |
|
|
|
|
|
(existingEntry) => { |
|
|
|
|
|
existingEntry.remove( |
|
|
|
|
|
() => { |
|
|
|
|
|
sourceFileEntry.copyTo( |
|
|
|
|
|
dataDirEntry, |
|
|
|
|
|
mediaFile.name, |
|
|
|
|
|
(newFileEntry) => { |
|
|
|
|
|
const fileUrl = newFileEntry.toURL() |
|
|
|
|
|
console.log('[w6sMedia] 重试复制成功, fileUrl:', fileUrl) |
|
|
|
|
|
resolve(fileUrl) |
|
|
|
|
|
}, |
|
|
|
|
|
reject |
|
|
|
|
|
) |
|
|
|
|
|
}, |
|
|
|
|
|
() => { |
|
|
|
|
|
// 无法删除旧文件,直接 resolve 原始 localURL
|
|
|
|
|
|
console.warn('[w6sMedia] 无法删除旧文件,返回原始 localURL') |
|
|
|
|
|
resolve(mediaFile.localURL) |
|
|
|
|
|
} |
|
|
|
|
|
) |
|
|
|
|
|
}, |
|
|
|
|
|
() => { |
|
|
|
|
|
// 旧文件不存在,直接报错
|
|
|
|
|
|
reject(copyErr) |
|
|
|
|
|
} |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
} |
|
|
) |
|
|
) |
|
|
}, |
|
|
}, |
|
|
reject |
|
|
(dataDirErr) => { |
|
|
|
|
|
console.error('[w6sMedia] resolve dataDirectory 失败:', dataDirErr) |
|
|
|
|
|
reject(dataDirErr) |
|
|
|
|
|
} |
|
|
) |
|
|
) |
|
|
}, |
|
|
}, |
|
|
(fileErr) => { |
|
|
(sourceErr) => { |
|
|
console.error('[w6sMedia] MediaFile.file() 失败:', fileErr) |
|
|
console.error('[w6sMedia] resolve source localURL 失败:', sourceErr) |
|
|
reject(fileErr) |
|
|
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 }) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// 录制视频(使用 Cordova 原生 Media Capture API)
|
|
|
// 录制视频(使用 Cordova 原生 Media Capture API)
|
|
|
export const startVideoRecorder = async ({ |
|
|
export const startVideoRecorder = async ({ |
|
|
duration = 0, |
|
|
duration = 0, |
|
|
quality = 1, // 0 高清 / 1 一般 / 2 流畅
|
|
|
quality = 1, |
|
|
front = false, |
|
|
front = false, |
|
|
syncSystemAlbum = false, |
|
|
syncSystemAlbum = false, |
|
|
} = {}) => { |
|
|
} = {}) => { |
|
|
@ -192,8 +232,9 @@ export const startVideoRecorder = async ({ |
|
|
if (mediaFiles && mediaFiles.length > 0) { |
|
|
if (mediaFiles && mediaFiles.length > 0) { |
|
|
const file = mediaFiles[0] |
|
|
const file = mediaFiles[0] |
|
|
console.log('[w6sMedia] 视频文件: fullPath=', file.fullPath, 'localURL=', file.localURL, 'name=', file.name, 'size=', file.size) |
|
|
console.log('[w6sMedia] 视频文件: fullPath=', file.fullPath, 'localURL=', file.localURL, 'name=', file.name, 'size=', file.size) |
|
|
|
|
|
// 优先把文件复制到应用私有目录,后续上传时传复制后的路径
|
|
|
|
|
|
// 如果复制失败,fallback 返回原始 localURL,让上传端再尝试 fetch+Blob 方案
|
|
|
try { |
|
|
try { |
|
|
// 尝试将 MediaFile 复制到应用私有目录,获得可访问的 file:// URL
|
|
|
|
|
|
const appDirPath = await copyMediaFileToAppDir(file) |
|
|
const appDirPath = await copyMediaFileToAppDir(file) |
|
|
console.log('[w6sMedia] 视频已复制到应用私有目录:', appDirPath) |
|
|
console.log('[w6sMedia] 视频已复制到应用私有目录:', appDirPath) |
|
|
resolve({ |
|
|
resolve({ |
|
|
@ -201,14 +242,16 @@ export const startVideoRecorder = async ({ |
|
|
name: file.name, |
|
|
name: file.name, |
|
|
size: file.size, |
|
|
size: file.size, |
|
|
type: file.type || 'video/mp4', |
|
|
type: file.type || 'video/mp4', |
|
|
|
|
|
_mediaFile: file, // 附带原始 MediaFile,供 fetch+Blob 上传兜底
|
|
|
}) |
|
|
}) |
|
|
} catch (copyErr) { |
|
|
} catch (copyErr) { |
|
|
console.warn('[w6sMedia] 复制视频失败,尝试使用 localURL:', copyErr) |
|
|
console.warn('[w6sMedia] 复制视频到应用私有目录失败,返回原始 MediaFile 作兜底:', copyErr) |
|
|
resolve({ |
|
|
resolve({ |
|
|
video_path: file.localURL, |
|
|
video_path: file.localURL, |
|
|
name: file.name, |
|
|
name: file.name, |
|
|
size: file.size, |
|
|
size: file.size, |
|
|
type: file.type || 'video/mp4', |
|
|
type: file.type || 'video/mp4', |
|
|
|
|
|
_mediaFile: file, |
|
|
}) |
|
|
}) |
|
|
} |
|
|
} |
|
|
} else { |
|
|
} else { |
|
|
@ -233,20 +276,41 @@ export const getLocation = async () => { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 上传本地文件到后端 /app-enf/common/file/upload
|
|
|
// 上传本地文件到后端 /app-enf/common/file/upload
|
|
|
// 使用 w6s.file.upload(Cordova FileTransfer),直接传本地路径,无需 base64 转换
|
|
|
// 默认使用 w6s.file.upload(Cordova FileTransfer)
|
|
|
|
|
|
// 如果传入 mediaFile(视频场景),FileTransfer 失败(如 EACCES)时自动 fallback 到 fetch+Blob 方案
|
|
|
export const uploadLocalFileToBackend = async ({ |
|
|
export const uploadLocalFileToBackend = async ({ |
|
|
fileURL, |
|
|
fileURL, |
|
|
fileName = 'upload', |
|
|
fileName = 'upload', |
|
|
mimeType = 'image/jpeg', |
|
|
mimeType = 'image/jpeg', |
|
|
|
|
|
mediaFile = null, |
|
|
}) => { |
|
|
}) => { |
|
|
console.log('[w6sMedia] uploadLocalFileToBackend 开始, 入参:', { fileURL, fileName, mimeType }) |
|
|
console.log('[w6sMedia] uploadLocalFileToBackend 开始, 入参:', { fileURL, fileName, mimeType, hasMediaFile: !!mediaFile }) |
|
|
await ensureW6sAuth() |
|
|
await ensureW6sAuth() |
|
|
console.log('[w6sMedia] 上传前鉴权完成') |
|
|
|
|
|
|
|
|
// 优先用 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) => { |
|
|
return new Promise((resolve, reject) => { |
|
|
const token = $globalStore.useCommon?.token || '' |
|
|
const token = $globalStore.useCommon?.token || '' |
|
|
const api = `${baseUrl}/app-enf/common/file/upload` |
|
|
const api = `${baseUrl}/app-enf/common/file/upload` |
|
|
const server = /^https?:/.test(api) ? api : `${window.location.origin}${api}` |
|
|
const server = /^https?:/.test(api) ? api : `${window.location.origin}${api}` |
|
|
console.log('[w6sMedia] 上传目标 server:', server, 'token 存在:', !!token) |
|
|
console.log('[w6sMedia] FileTransfer 上传, server:', server, 'fileURL:', fileURL) |
|
|
|
|
|
|
|
|
const opts = new window.FileUploadOptions() |
|
|
const opts = new window.FileUploadOptions() |
|
|
opts.fileKey = 'file' |
|
|
opts.fileKey = 'file' |
|
|
@ -256,7 +320,6 @@ export const uploadLocalFileToBackend = async ({ |
|
|
Authorization: token ? `Bearer ${token}` : '', |
|
|
Authorization: token ? `Bearer ${token}` : '', |
|
|
clientid: '9e6eb24cd4e169a98b0d2cd8034d877b', |
|
|
clientid: '9e6eb24cd4e169a98b0d2cd8034d877b', |
|
|
} |
|
|
} |
|
|
console.log('[w6sMedia] FileUploadOptions 构造完成:', { fileKey: opts.fileKey, fileName: opts.fileName, mimeType: opts.mimeType }) |
|
|
|
|
|
|
|
|
|
|
|
w6s.file.upload( |
|
|
w6s.file.upload( |
|
|
{ |
|
|
{ |
|
|
@ -264,25 +327,22 @@ export const uploadLocalFileToBackend = async ({ |
|
|
server, |
|
|
server, |
|
|
trustAllHosts: false, |
|
|
trustAllHosts: false, |
|
|
success: (res) => { |
|
|
success: (res) => { |
|
|
console.log('[w6sMedia] file.upload success 原始返回:', res) |
|
|
console.log('[w6sMedia] FileTransfer success 原始返回:', res) |
|
|
try { |
|
|
try { |
|
|
const parsed = JSON.parse(res.response) |
|
|
const parsed = JSON.parse(res.response) |
|
|
console.log('[w6sMedia] 解析后的上传响应:', parsed) |
|
|
|
|
|
resolve(parsed) |
|
|
resolve(parsed) |
|
|
} catch (e) { |
|
|
} catch { |
|
|
console.warn('[w6sMedia] 响应 JSON 解析失败, 返回原始 response:', res.response) |
|
|
|
|
|
resolve(res.response) |
|
|
resolve(res.response) |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
fail: (err) => { |
|
|
fail: (err) => { |
|
|
console.error('[w6sMedia] file.upload fail:', err) |
|
|
console.error('[w6sMedia] FileTransfer fail:', err) |
|
|
reject(err) |
|
|
reject(err) |
|
|
}, |
|
|
}, |
|
|
}, |
|
|
}, |
|
|
opts |
|
|
opts |
|
|
).catch((err) => { |
|
|
).catch((err) => { |
|
|
// w6s.file.upload 在 responseCode !== 200 时会 reject 但不调用 fail 回调
|
|
|
console.error('[w6sMedia] FileTransfer Promise reject:', err) |
|
|
console.error('[w6sMedia] w6s.file.upload Promise reject:', err) |
|
|
|
|
|
reject(err) |
|
|
reject(err) |
|
|
}) |
|
|
}) |
|
|
}) |
|
|
}) |
|
|
|