You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
328 lines
13 KiB
328 lines
13 KiB
import * as w6s from '@w6s/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()
|
|
|
|
// SDK 默认接口超时仅 5s,getLocation 等 API 未在 5s 内回调会报"接口调用超时"
|
|
// 且 takePhotoAndAddWaterMark 内部会先调 getLocation,必须放宽超时
|
|
w6s.init({
|
|
auth: true,
|
|
timeout: 5 * 60 * 1000, // 5 分钟,覆盖拍照+定位全流程
|
|
})
|
|
|
|
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
|
|
}
|
|
|
|
// 定位缓存(避免每次拍照都重新搜星,SDK 内部 locationEnable=true 时会阻塞等 GPS)
|
|
let _cachedLocation = null
|
|
let _locationCacheTime = 0
|
|
const LOCATION_CACHE_TTL = 2 * 60 * 1000 // 2 分钟内复用同一位置
|
|
|
|
const getCachedLocation = async () => {
|
|
if (_cachedLocation && Date.now() - _locationCacheTime < LOCATION_CACHE_TTL) {
|
|
console.log('[w6sMedia] 使用缓存定位:', _cachedLocation)
|
|
return _cachedLocation
|
|
}
|
|
await ensureW6sAuth()
|
|
_cachedLocation = await new Promise((resolve, reject) => {
|
|
w6s.location.getLocation({ success: resolve, fail: reject })
|
|
})
|
|
_locationCacheTime = Date.now()
|
|
console.log('[w6sMedia] 定位获取成功并已缓存:', _cachedLocation)
|
|
return _cachedLocation
|
|
}
|
|
|
|
// 格式化时间 yyyy-MM-dd HH:mm(与 SDK 内部 getTimeStr 格式一致)
|
|
const getTimeStr = () => {
|
|
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())}`
|
|
}
|
|
|
|
// 去掉 file:// 前缀(w6s 的 toBase64 在 Android 下会自动补 file://,避免重复前缀导致解析失败)
|
|
const stripFileScheme = (p = '') => String(p).replace(/^file:\/\//, '')
|
|
|
|
// 水印图最长边上限(像素)。原图往往 4000+,整张重新编码是耗时主因,
|
|
// 限制到 2048 可大幅缩短耗时并减小上传体积;需要更高清可上调
|
|
const WATERMARK_MAX_SIZE = 2048
|
|
|
|
// 读取水印源图:优先用本地文件 URL(cdvfile:// 由 Cordova 拦截读取),无需把整张图转成 base64;
|
|
// 环境/路径不支持时回退到 SDK 的 toBase64
|
|
const loadWatermarkSource = async (filePath) => {
|
|
const toImg = (src) => new Promise((resolve, reject) => {
|
|
const el = document.createElement('img')
|
|
el.onload = () => resolve(el)
|
|
el.onerror = () => reject(new Error('水印源图片加载失败'))
|
|
el.src = src
|
|
})
|
|
const raw = stripFileScheme(filePath)
|
|
if (window.resolveLocalFileSystemURL) {
|
|
const fileUrl = /^(cdvfile|content):/i.test(String(filePath)) ? filePath : `file://${raw}`
|
|
try {
|
|
const entry = await new Promise((resolve, reject) => window.resolveLocalFileSystemURL(fileUrl, resolve, reject))
|
|
return await toImg(entry.toURL())
|
|
} catch (e) {
|
|
console.warn('[w6sMedia] 本地文件 URL 加载失败, 回退 toBase64:', e)
|
|
}
|
|
}
|
|
const evt = await w6s.image.toBase64({ filePath: raw })
|
|
return toImg(evt?.target?.result || evt?.result || evt)
|
|
}
|
|
|
|
// 前端自行给图片叠加时间水印,返回带水印图片的本地文件路径
|
|
// 说明:w6s.image.takePhotoAndAddWaterMark 实测不可用(原生加水印接口无有效返回),
|
|
// 改为拍照后用 canvas 在前端叠加时间水印;用 toBlob 直接落盘,省去 base64 编解码开销
|
|
const addTimeWaterMark = async (filePath, fileName) => {
|
|
console.log('[w6sMedia] 开始叠加时间水印, 源图:', filePath)
|
|
const img = await loadWatermarkSource(filePath)
|
|
|
|
// 按最长边等比缩放,避免整张原图重新编码(编码是耗时主因)
|
|
const srcW = img.naturalWidth || img.width
|
|
const srcH = img.naturalHeight || img.height
|
|
const scale = Math.min(1, WATERMARK_MAX_SIZE / Math.max(srcW, srcH))
|
|
const canvas = document.createElement('canvas')
|
|
canvas.width = Math.round(srcW * scale)
|
|
canvas.height = Math.round(srcH * scale)
|
|
const ctx = canvas.getContext('2d')
|
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
|
|
|
const text = getTimeStr()
|
|
const fontSize = Math.max(16, Math.round(canvas.width * 0.035))
|
|
const pad = Math.round(fontSize * 0.6)
|
|
ctx.font = `${fontSize}px sans-serif`
|
|
ctx.textAlign = 'left'
|
|
ctx.textBaseline = 'bottom'
|
|
// 描边提升浅色背景下的可读性
|
|
ctx.lineWidth = Math.max(2, Math.round(fontSize * 0.12))
|
|
ctx.strokeStyle = 'rgba(0, 0, 0, 0.6)'
|
|
ctx.fillStyle = '#FF5858'
|
|
const x = pad
|
|
const y = canvas.height - pad
|
|
ctx.strokeText(text, x, y)
|
|
ctx.fillText(text, x, y)
|
|
|
|
// toBlob 直接产出二进制,避免 toDataURL 的 base64 编码
|
|
const blob = await new Promise((resolve, reject) => {
|
|
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('水印图片编码失败'))), 'image/jpeg', 0.9)
|
|
})
|
|
const out = await writeBlobToLocalFile(blob, fileName)
|
|
console.log('[w6sMedia] 时间水印叠加完成, 输出:', out, '大小:', blob.size)
|
|
return out
|
|
}
|
|
|
|
// 拍照并加水印
|
|
// 注:w6s.image.takePhotoAndAddWaterMark 不可用,已改为 w6s.image.takePicture + 前端 canvas 叠加时间水印
|
|
export const takePhotoWithWaterMark = async () => {
|
|
await ensureW6sAuth()
|
|
console.log('[w6sMedia] 鉴权完成, 准备调起拍照')
|
|
|
|
// 1. 调起原生拍照,返回 { imageURL(压缩图本地路径), key(原图本地路径), imageInfo, mediaId }
|
|
const photo = await w6s.image.takePicture({ editable: false })
|
|
.then((res) => {
|
|
console.log('[w6sMedia] takePicture success 原始返回:', res)
|
|
const result = res?.result || res || {}
|
|
console.log('[w6sMedia] 拍照返回 imageURL:', result?.imageURL, 'key:', result?.key)
|
|
return result
|
|
})
|
|
.catch((err) => {
|
|
console.error('[w6sMedia] takePicture fail:', err)
|
|
throw err
|
|
})
|
|
|
|
// 2. 前端叠加时间水印,产出带水印的图片本地路径
|
|
const sourcePath = photo.imageURL || photo.key
|
|
const imageURL = await addTimeWaterMark(sourcePath, `wm-photo-${Date.now()}.jpg`)
|
|
|
|
// 3. 返回结构与原来一致(imageURL 为带水印图片的本地路径,可直接上传)
|
|
return { ...photo, imageURL }
|
|
}
|
|
|
|
// 录制视频(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] startVideoRecorder 开始, 入参:', { duration, quality, front, syncSystemAlbum })
|
|
await ensureW6sAuth()
|
|
console.log('[w6sMedia] 录制前鉴权完成, 准备调起录制')
|
|
return new Promise((resolve, reject) => {
|
|
w6s.video.startVideoRecoder({
|
|
duration,
|
|
quality: String(quality),
|
|
sync_system_album: syncSystemAlbum,
|
|
front,
|
|
success: (res) => {
|
|
console.log('[w6sMedia] startVideoRecoder success 原始返回:', res)
|
|
// 实际返回结构为 { result: { code, message, info } }(SDK jsonParser 会包一层 result),而非 d.ts 声明的 { info }
|
|
const result = res?.result || res || {}
|
|
const info = result?.info || {}
|
|
if (!info.video_path) {
|
|
reject(new Error('录制未生成视频文件'))
|
|
return
|
|
}
|
|
// 客户端返回的是裸绝对路径(如 /data/data/.../xxx.mp4),FileTransfer 需要 file:// 前缀
|
|
const rawPath = info.video_path
|
|
const videoPath = /^file:\/\//.test(rawPath) ? rawPath : `file://${rawPath}`
|
|
// 从路径提取文件名(可能带查询参数)
|
|
const name = (videoPath.split('/').pop() || '').split('?')[0] || `video-${Date.now()}.mp4`
|
|
console.log('[w6sMedia] 解析出 video_path:', videoPath, 'name:', name)
|
|
resolve({
|
|
video_path: videoPath,
|
|
video_duration: info.video_duration,
|
|
video_size: info.video_size,
|
|
video_thumbnail: info.video_thumbnail,
|
|
name,
|
|
type: 'video/mp4',
|
|
})
|
|
},
|
|
fail: (err) => {
|
|
console.error('[w6sMedia] startVideoRecoder fail:', err)
|
|
reject(err)
|
|
},
|
|
})
|
|
})
|
|
}
|
|
|
|
// 获取定位(经纬度)
|
|
export const getLocation = async () => {
|
|
await ensureW6sAuth()
|
|
return new Promise((resolve, reject) => {
|
|
w6s.location.getLocation({ success: resolve, fail: reject })
|
|
})
|
|
}
|
|
|
|
// 将 Blob 写入客户端缓存目录,返回可上传的本地文件路径
|
|
// FileTransfer 只支持文件路径,canvas 产出的图片 Blob 需先落盘
|
|
const writeBlobToLocalFile = (blob, fileName) => new Promise((resolve, reject) => {
|
|
const cordovaFile = window.cordova?.file
|
|
if (!window.resolveLocalFileSystemURL || !cordovaFile?.cacheDirectory) {
|
|
reject(new Error('当前环境不支持写入本地文件(缺少 cordova-plugin-file)'))
|
|
return
|
|
}
|
|
window.resolveLocalFileSystemURL(cordovaFile.cacheDirectory, (dirEntry) => {
|
|
dirEntry.getFile(fileName, { create: true }, (fileEntry) => {
|
|
fileEntry.createWriter((writer) => {
|
|
writer.onwriteend = () => resolve(fileEntry.toURL())
|
|
writer.onerror = (e) => reject(e)
|
|
writer.write(blob)
|
|
}, reject)
|
|
}, reject)
|
|
}, reject)
|
|
})
|
|
|
|
// base64 dataURL → Blob(仅作为兼容兜底,正常流程已用 toBlob 直接产出二进制)
|
|
const dataUrlToBlob = (dataUrl) => {
|
|
const commaIdx = dataUrl.indexOf(',')
|
|
const type = (dataUrl.slice(0, commaIdx).match(/data:(.*?);/) || [])[1] || 'image/jpeg'
|
|
const bin = atob(dataUrl.slice(commaIdx + 1))
|
|
const bytes = new Uint8Array(bin.length)
|
|
for (let i = 0; i < bin.length; i += 1) bytes[i] = bin.charCodeAt(i)
|
|
return new Blob([bytes], { type })
|
|
}
|
|
|
|
const writeDataUrlToFile = (dataUrl, fileName) => writeBlobToLocalFile(dataUrlToBlob(dataUrl), fileName)
|
|
|
|
// 上传本地文件到后端 /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 实际实现(@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}`
|
|
|
|
// base64 dataURL(如拍照后前端叠加水印的图片)先落盘为本地临时文件
|
|
const localFileURL = /^data:/.test(fileURL)
|
|
? await writeDataUrlToFile(fileURL, fileName)
|
|
: fileURL
|
|
console.log('[w6sMedia] FileTransfer 上传, server:', server, 'fileURL:', localFileURL)
|
|
|
|
const opts = {
|
|
fileKey: 'file',
|
|
fileName,
|
|
mimeType,
|
|
headers: {
|
|
Authorization: token ? `Bearer ${token}` : '',
|
|
clientid: '9e6eb24cd4e169a98b0d2cd8034d877b',
|
|
},
|
|
}
|
|
|
|
const uploader = new w6s.file.upload(opts)
|
|
// upload 返回 Promise:responseCode === 200 时 resolve,否则 reject
|
|
const res = await uploader.upload({
|
|
fileURL: localFileURL,
|
|
server,
|
|
trustAllHosts: false,
|
|
})
|
|
console.log('[w6sMedia] FileTransfer success 原始返回:', res)
|
|
try {
|
|
return JSON.parse(res.response)
|
|
} catch {
|
|
return res.response
|
|
}
|
|
}
|
|
|