diff --git a/zdxt-web-client/zdxt-efcode-enterprise-app/src/api/enterprise/index.js b/zdxt-web-client/zdxt-efcode-enterprise-app/src/api/enterprise/index.js index 597411c9..1af59f8c 100644 --- a/zdxt-web-client/zdxt-efcode-enterprise-app/src/api/enterprise/index.js +++ b/zdxt-web-client/zdxt-efcode-enterprise-app/src/api/enterprise/index.js @@ -78,6 +78,9 @@ export const addComplaint = (params, success, error) => _post(enterpriseAppBaseU //查询字典数据(公共) export const queryDict = (params, success, error) => _get("/system/dict/data/dictType",params,success,error) +//查询系统配置值(如 gps.check:1 开启定位,0 不开启) +export const getSystemConfigByKey = (configKey, params, success, error) => _get(`/system/config/configKey/${configKey}`, params, success, error) + //执法单位(部门)列表 export const getDeptList = (params, success, error) => _get("/enforcementcode/virtualUnit/list",params,success,error) diff --git a/zdxt-web-client/zdxt-efcode-enterprise-app/src/util/w6sMedia.js b/zdxt-web-client/zdxt-efcode-enterprise-app/src/util/w6sMedia.js index 9eb8cb14..e612859d 100644 --- a/zdxt-web-client/zdxt-efcode-enterprise-app/src/util/w6sMedia.js +++ b/zdxt-web-client/zdxt-efcode-enterprise-app/src/util/w6sMedia.js @@ -1,6 +1,6 @@ import * as w6s from '@w6s/sdk' import sha1 from 'crypto-js/sha1' -import { siqCode } from '@/api/enterprise' +import { siqCode, getSystemConfigByKey } from '@/api/enterprise' import { baseUrl } from '@/config/env' import { $globalStore } from '@/common/globalStore' @@ -42,7 +42,7 @@ export const ensureW6sAuth = () => { const signature = sha1(str).toString().toLowerCase() // SDK 默认接口超时仅 5s,getLocation 等 API 未在 5s 内回调会报"接口调用超时" - // 且 takePhotoAndAddWaterMark 内部会先调 getLocation,必须放宽超时 + // 拍照前需预取定位(GPS 搜星耗时),必须放宽超时 w6s.init({ auth: true, timeout: 5 * 60 * 1000, // 5 分钟,覆盖拍照+定位全流程 @@ -72,10 +72,10 @@ export const ensureW6sAuth = () => { return w6sAuthPromise } -// 定位缓存(避免每次拍照都重新搜星,SDK 内部 locationEnable=true 时会阻塞等 GPS) +// 定位缓存(避免每次拍照都重新搜星) let _cachedLocation = null let _locationCacheTime = 0 -const LOCATION_CACHE_TTL = 2 * 60 * 1000 // 2 分钟内复用同一位置 +const LOCATION_CACHE_TTL = 1 * 60 * 1000 // 1 分钟内复用同一位置 const getCachedLocation = async () => { if (_cachedLocation && Date.now() - _locationCacheTime < LOCATION_CACHE_TTL) { @@ -91,6 +91,48 @@ const getCachedLocation = async () => { return _cachedLocation } +// 水印用定位:加超时保护,避免 GPS 搜星过慢阻塞拍照加水印流程 +const LOCATION_TIMEOUT = 8 * 1000 +const getLocationForWaterMark = () => Promise.race([ + getCachedLocation(), + new Promise((resolve, reject) => { + setTimeout(() => reject(new Error('定位获取超时')), LOCATION_TIMEOUT) + }), +]) + +// 定位开关配置 gps.check:1 开启定位,0 不开启;缓存 5 分钟避免每次拍照都请求 +let _gpsEnabled = null +let _gpsCheckTime = 0 +let _gpsCheckPromise = null +const GPS_CHECK_TTL = 5 * 60 * 1000 + +const isGpsEnabled = () => { + if (_gpsEnabled !== null && Date.now() - _gpsCheckTime < GPS_CHECK_TTL) { + return Promise.resolve(_gpsEnabled) + } + if (!_gpsCheckPromise) { + _gpsCheckPromise = new Promise((resolve) => { + getSystemConfigByKey('gps.check', null, (res) => { + // 兼容 { code, msg, data: '1' } 包裹与直接返回 '1' + const value = res && typeof res === 'object' ? (res.data ?? res.msg) : res + _gpsEnabled = String(value ?? '').trim() === '1' + _gpsCheckTime = Date.now() + _gpsCheckPromise = null + console.log('[w6sMedia] gps.check =', value, '→ 定位', _gpsEnabled ? '开启' : '关闭') + resolve(_gpsEnabled) + }, (err) => { + // 配置获取失败时按不开启定位处理 + console.warn('[w6sMedia] 获取 gps.check 配置失败, 水印不含定位:', err) + _gpsEnabled = false + _gpsCheckTime = Date.now() + _gpsCheckPromise = null + resolve(false) + }) + }) + } + return _gpsCheckPromise +} + // 格式化时间 yyyy-MM-dd HH:mm(与 SDK 内部 getTimeStr 格式一致) const getTimeStr = () => { const d = new Date() @@ -98,102 +140,57 @@ const getTimeStr = () => { 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 +// 定位信息 → 水印文本(地址 + 经纬度),无定位时返回空串 +const formatLocationText = (loc) => { + if (!loc) return '' + const address = loc.address || [loc.city, loc.district, loc.street, loc.aoiName].filter(Boolean).join('') + const { longitude, latitude } = loc + const coords = longitude != null && latitude != null + ? `经度 ${Number(longitude).toFixed(6)} 纬度 ${Number(latitude).toFixed(6)}` + : '' + return [address, coords].filter(Boolean).join(' ') } -// 拍照并加水印 -// 注:w6s.image.takePhotoAndAddWaterMark 不可用,已改为 w6s.image.takePicture + 前端 canvas 叠加时间水印 +// 拍照并加水印(w6s 原生接口 takePhotoAndAddWaterMark) +// 时间与定位由前端拼入 content,timeEnable/locationEnable 置 false, +// 跳过 SDK 内部再次调 getLocation(该调用易超时阻塞拍照流程) export const takePhotoWithWaterMark = async () => { await ensureW6sAuth() - console.log('[w6sMedia] 鉴权完成, 准备调起拍照') + 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 - }) + // 按 gps.check 配置决定是否加水印定位;配置为 0 时完全跳过 getLocation + let locationText = '' + try { + if (await isGpsEnabled()) { + locationText = formatLocationText(await getLocationForWaterMark()) + } else { + console.log('[w6sMedia] gps.check=0, 水印不含定位') + } + } catch (e) { + console.warn('[w6sMedia] 获取定位失败, 水印不含定位:', e) + } - // 2. 前端叠加时间水印,产出带水印的图片本地路径 - const sourcePath = photo.imageURL || photo.key - const imageURL = await addTimeWaterMark(sourcePath, `wm-photo-${Date.now()}.jpg`) + const content = [getTimeStr(), locationText].filter(Boolean).join(' ') + console.log('[w6sMedia] 水印 content:', content, '准备调起拍照') - // 3. 返回结构与原来一致(imageURL 为带水印图片的本地路径,可直接上传) - return { ...photo, imageURL } + return new Promise((resolve, reject) => { + w6s.image.takePhotoAndAddWaterMark({ + content, + fontSize: 14, + color: '#FF5858', + markDisable: false, + timeEnable: false, // 时间已拼入 content + locationEnable: false, // 定位已拼入 content,跳过 SDK 内部 getLocation + success: (res) => { + console.log('[w6sMedia] takePhotoAndAddWaterMark success 原始返回:', res) + resolve(res?.result || res || {}) + }, + fail: (err) => { + console.error('[w6sMedia] takePhotoAndAddWaterMark fail:', err) + reject(err) + }, + }) + }) } // 录制视频(w6s 官方 API,文档 https://open.beeworks.cn/js-sdk/video.html)