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 } }