|
|
|
|
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 })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 录制视频(使用 Cordova 原生 Media Capture API)
|
|
|
|
|
export const startVideoRecorder = async ({
|
|
|
|
|
duration = 0,
|
|
|
|
|
quality = 1,
|
|
|
|
|
front = false,
|
|
|
|
|
syncSystemAlbum = false,
|
|
|
|
|
} = {}) => {
|
|
|
|
|
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]
|
|
|
|
|
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)
|
|
|
|
|
resolve({
|
|
|
|
|
video_path: appDirPath,
|
|
|
|
|
name: file.name,
|
|
|
|
|
size: file.size,
|
|
|
|
|
type: file.type || 'video/mp4',
|
|
|
|
|
_mediaFile: file, // 附带原始 MediaFile,供 fetch+Blob 上传兜底
|
|
|
|
|
})
|
|
|
|
|
} 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 }
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取定位(经纬度)
|
|
|
|
|
export const getLocation = async () => {
|
|
|
|
|
await ensureW6sAuth()
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
w6s.location.getLocation({ success: resolve, fail: reject })
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 上传本地文件到后端 /app-enf/common/file/upload
|
|
|
|
|
// 默认使用 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, hasMediaFile: !!mediaFile })
|
|
|
|
|
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)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|