Browse Source

视频上传

dev-V1.10.2-temp
wangmingyong@163.com 2 months ago
parent
commit
36a6366da8
  1. 170
      zdxt-web-client/zdxt-efcode-enterprise-app/src/util/w6sMedia.js
  2. 11
      zdxt-web-client/zdxt-efcode-enterprise-app/src/views/enterprise/nos/components/NosDetailMaterialsTab.vue

170
zdxt-web-client/zdxt-efcode-enterprise-app/src/util/w6sMedia.js

@ -100,80 +100,120 @@ export const takePhotoWithWaterMark = async () => {
}
// 将 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) => {
return new Promise((resolve, reject) => {
if (!window.cordova?.file?.dataDirectory) {
reject(new Error('cordova.file.dataDirectory 不可用'))
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 更可靠)
mediaFile.file(
window.resolveLocalFileSystemURL(
sourceLocalURL,
(sourceFileEntry) => {
console.log('[w6sMedia] 获取源 FileEntry 成功:', sourceFileEntry.fullPath)
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,
mediaFile.name,
safeName,
(newFileEntry) => {
const fileUrl = newFileEntry.toURL()
console.log('[w6sMedia] 文件已复制到应用私有目录, fileUrl:', fileUrl)
resolve(fileUrl)
},
(copyErr) => {
// 目标文件已存在时,先删除再复制
console.warn('[w6sMedia] copyTo 可能目标已存在,尝试移除后重试:', 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)
}
)
console.error('[w6sMedia] copyTo 失败:', copyErr)
reject(copyErr)
}
)
},
reject
(dataDirErr) => {
console.error('[w6sMedia] resolve dataDirectory 失败:', dataDirErr)
reject(dataDirErr)
}
)
},
(fileErr) => {
console.error('[w6sMedia] MediaFile.file() 失败:', fileErr)
reject(fileErr)
(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 })
}
// 录制视频(使用 Cordova 原生 Media Capture API)
export const startVideoRecorder = async ({
duration = 0,
quality = 1, // 0 高清 / 1 一般 / 2 流畅
quality = 1,
front = false,
syncSystemAlbum = false,
} = {}) => {
@ -192,8 +232,9 @@ export const startVideoRecorder = async ({
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 {
// 尝试将 MediaFile 复制到应用私有目录,获得可访问的 file:// URL
const appDirPath = await copyMediaFileToAppDir(file)
console.log('[w6sMedia] 视频已复制到应用私有目录:', appDirPath)
resolve({
@ -201,14 +242,16 @@ export const startVideoRecorder = async ({
name: file.name,
size: file.size,
type: file.type || 'video/mp4',
_mediaFile: file, // 附带原始 MediaFile,供 fetch+Blob 上传兜底
})
} catch (copyErr) {
console.warn('[w6sMedia] 复制视频失败,尝试使用 localURL:', 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 {
@ -233,20 +276,41 @@ export const getLocation = async () => {
}
// 上传本地文件到后端 /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 ({
fileURL,
fileName = 'upload',
mimeType = 'image/jpeg',
mediaFile = null,
}) => {
console.log('[w6sMedia] uploadLocalFileToBackend 开始, 入参:', { fileURL, fileName, mimeType })
console.log('[w6sMedia] uploadLocalFileToBackend 开始, 入参:', { fileURL, fileName, mimeType, hasMediaFile: !!mediaFile })
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) => {
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] 上传目标 server:', server, 'token 存在:', !!token)
console.log('[w6sMedia] FileTransfer 上传, server:', server, 'fileURL:', fileURL)
const opts = new window.FileUploadOptions()
opts.fileKey = 'file'
@ -256,7 +320,6 @@ export const uploadLocalFileToBackend = async ({
Authorization: token ? `Bearer ${token}` : '',
clientid: '9e6eb24cd4e169a98b0d2cd8034d877b',
}
console.log('[w6sMedia] FileUploadOptions 构造完成:', { fileKey: opts.fileKey, fileName: opts.fileName, mimeType: opts.mimeType })
w6s.file.upload(
{
@ -264,25 +327,22 @@ export const uploadLocalFileToBackend = async ({
server,
trustAllHosts: false,
success: (res) => {
console.log('[w6sMedia] file.upload success 原始返回:', res)
console.log('[w6sMedia] FileTransfer success 原始返回:', res)
try {
const parsed = JSON.parse(res.response)
console.log('[w6sMedia] 解析后的上传响应:', parsed)
resolve(parsed)
} catch (e) {
console.warn('[w6sMedia] 响应 JSON 解析失败, 返回原始 response:', res.response)
} catch {
resolve(res.response)
}
},
fail: (err) => {
console.error('[w6sMedia] file.upload fail:', err)
console.error('[w6sMedia] FileTransfer fail:', err)
reject(err)
},
},
opts
).catch((err) => {
// w6s.file.upload 在 responseCode !== 200 时会 reject 但不调用 fail 回调
console.error('[w6sMedia] w6s.file.upload Promise reject:', err)
console.error('[w6sMedia] FileTransfer Promise reject:', err)
reject(err)
})
})

11
zdxt-web-client/zdxt-efcode-enterprise-app/src/views/enterprise/nos/components/NosDetailMaterialsTab.vue

@ -457,13 +457,14 @@ const onTakePhoto = async (item) => {
const onRecordVideo = async (item) => {
console.log('[NosDetail] onRecordVideo 开始, item:', item)
try {
const res = await startVideoRecorder({ duration: 60, quality: 1 })
const res = await startVideoRecorder({ duration: 0, quality: 1 })
console.log('[NosDetail] 录制返回 res:', res, 'video_path:', res?.video_path)
await pushSdkFileToFileList(item, {
localPath: res.video_path,
fileName: `video-${Date.now()}.mp4`,
mimeType: res.type || 'video/mp4',
isImage: false,
mediaFile: res._mediaFile,
})
} catch (err) {
console.error('[NosDetail] w6s 录制失败:', err)
@ -472,8 +473,9 @@ const onRecordVideo = async (item) => {
}
// 通用:把 SDK 返回的本地文件上传后端 → 写入 fileList
const pushSdkFileToFileList = async (item, { localPath, fileName, mimeType, isImage }) => {
console.log('[NosDetail] pushSdkFileToFileList 开始, 入参:', { localPath, fileName, mimeType, isImage })
// 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 为空, 直接返回')
return
@ -485,11 +487,10 @@ const pushSdkFileToFileList = async (item, { localPath, fileName, mimeType, isIm
message: '上传中...',
}
item.fileList.push(fileObj)
// 取数组中的响应式引用,避免后续直接修改原始普通对象导致视图不更新
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 })
const res = await uploadLocalFileToBackend({ fileURL: localPath, fileName, mimeType, mediaFile })
console.log('[NosDetail] 上传后端返回 res:', res)
const data = res && res.data ? res.data : res
const filePath = typeof data === 'string'

Loading…
Cancel
Save