Browse Source

视频上传

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

99
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 后,<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,
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(图片已验证能成功)

25
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'

Loading…
Cancel
Save