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.
12 KiB
12 KiB
上传功能按环境拆分:本地用 van-uploader,生产强制现场拍摄/录制
Context(背景)
NosDetailMaterialsTab.vue 第 48-63 行使用单个 van-uploader 同时处理图片/视频/PDF。生产环境(beeworks 容器/深 i 企)内合规要求:线上不能用相册的图片和视频,必须是现场拍摄和录制,且拍照必须带水印与经纬度。
按环境拆分上传入口:
- 开发环境(dev):保留现有
van-uploader不变(支持图片/视频/PDF)。 - 生产环境(prod):只允许两种方式,均来自设备现场采集:
- 拍照 →
w6s.image.takePhotoAndAddWaterMark(timeEnable: true, locationEnable: true自动叠加时间戳与经纬度水印) - 录制视频 →
w6s.video.startVideoRecoder - 上传到后端 →
w6s.file.upload(原生 FileTransfer,直接传本地路径,无需 base64 转换) - 经纬度获取(可选) →
w6s.location.getLocation
- 拍照 →
关键约束:生产环境不提供相册选择、文件选择器入口,避免用户上传已有图片/视频。PDF 上传在生产环境是否保留待用户确认(见开放问题 1)。
关键复用点
- 环境判断:src/config/env.js 的
appEnv,生产值为'prod'。 - w6s SDK 导入:
import * as w6s from 'szient-js-sdk'(参考 qrCode.vue#L42)。鉴权已在 enterpriseHome.vue#L295-L344 完成。 - 后端上传地址:src/util/fileUpload.js 中
/app-enf/common/file/upload,完整 URL 为${baseUrl}/app-enf/common/file/upload。 - 认证头:src/http/http.js#L26-L31 拦截器加
Authorization: Bearer + token与clientid: 9e6eb24cd4e169a98b0d2cd8034d877b,w6s.file.upload需手动传入。 - URL 拼接:复用 src/util/fileUrl.js 的
toFullFileUrl。 - 全局状态:
$globalStore.useCommon(取token、enterpriseName)。
实施步骤
步骤 1:新建 src/util/w6sMedia.js
Promise 化封装 4 个方法:
import * as w6s from 'szient-js-sdk'
import { baseUrl } from '@/config/env'
import { $globalStore } from '@/common/globalStore'
// 1. 拍照并加水印(时间戳 + 经纬度自动叠加)
export const takePhotoWithWaterMark = ({
content = '',
fontSize = 14,
color = '#FF5858',
timeEnable = true,
locationEnable = true,
} = {}) => new Promise((resolve, reject) => {
w6s.image.takePhotoAndAddWaterMark({
content, fontSize, color,
markDisable: false,
timeEnable, locationEnable,
success: resolve,
fail: reject,
})
})
// 2. 录制视频
export const startVideoRecorder = ({
duration = 60,
quality = 1, // 0 高清 / 1 一般 / 2 流畅
front = false,
syncSystemAlbum = false,
} = {}) => new Promise((resolve, reject) => {
w6s.video.startVideoRecoder({
duration,
quality,
front,
sync_system_album: syncSystemAlbum,
success: resolve,
fail: reject,
})
})
// 3. 获取定位(可选,用于单独保存经纬度元数据)
export const getLocation = () => new Promise((resolve, reject) => {
w6s.location.getLocation({ success: resolve, fail: reject })
})
// 4. 上传本地文件到后端 /app-enf/common/file/upload
export const uploadLocalFileToBackend = ({
fileURL, // SDK 返回的本地路径
fileName = 'upload',
mimeType = 'image/jpeg',
}) => new Promise((resolve, reject) => {
const token = $globalStore.useCommon.token || ''
// w6s.file.upload 需要完整 URL
const api = `${baseUrl}/app-enf/common/file/upload`
const server = /^https?:/.test(api) ? api : `${window.location.origin}${api}`
const opts = new window.FileUploadOptions()
opts.fileKey = 'file'
opts.fileName = fileName
opts.mimeType = mimeType
opts.headers = {
'Authorization': token ? `Bearer ${token}` : '',
'clientid': '9e6eb24cd4e169a98b0d2cd8034d877b',
}
const fileIns = new w6s.file.upload(opts)
fileIns.upload({
fileURL,
server,
trustAllHosts: false,
success: (res) => {
// Cordova FileTransfer 返回 res.response 为字符串
try {
const parsed = typeof res.response === 'string' ? JSON.parse(res.response) : res.response
resolve(parsed)
} catch (e) {
resolve(res.response)
}
},
fail: reject,
})
})
步骤 2:修改 NosDetailMaterialsTab.vue
2.1 新增引入
import { appEnv } from '@/config/env'
import {
takePhotoWithWaterMark,
startVideoRecorder,
uploadLocalFileToBackend,
} from '@/util/w6sMedia'
const isProd = computed(() => appEnv === 'prod')
const showUploadAction = ref(false)
const activeUploadItem = ref(null)
const uploadActions = [
{ name: '拍照(带水印与经纬度)' },
{ name: '录制视频' },
]
2.2 模板改造(替换第 47-64 行)
<div class="material-media-edit" v-else>
<!-- 开发环境:原 van-uploader 不变 -->
<van-uploader
v-if="!isProd"
v-model="item.fileList"
multiple
:max-count="20"
:max-size="200 * 1024 * 1024"
accept="image/*,video/*,.pdf"
:after-read="onAfterReadImage"
:before-read="onBeforeRead"
@oversize="onOversize"
>
<template #default>
<div class="uploader-custom-btn">
<van-icon name="photograph" size="24" color="#dcdee0" />
</div>
</template>
</van-uploader>
<!-- 生产环境:自定义上传按钮 -->
<div v-else class="uploader-custom-btn" @click="onOpenUploadAction(item)">
<van-icon name="photograph" size="24" color="#dcdee0" />
</div>
</div>
<!-- 与现有 van-popup 同级 -->
<van-action-sheet
v-model:show="showUploadAction"
:actions="uploadActions"
cancel-text="取消"
close-on-click-action
@select="onSelectUploadAction"
/>
2.3 脚本逻辑
const onOpenUploadAction = (item) => {
if ((item.fileList?.length || 0) >= 20) {
showToast('已达到最多 20 个文件上限')
return
}
activeUploadItem.value = item
showUploadAction.value = true
}
const onSelectUploadAction = ({ name }) => {
const item = activeUploadItem.value
if (!item) return
if (name === '拍照(带水印与经纬度)') onTakePhoto(item)
else if (name === '录制视频') onRecordVideo(item)
}
// 拍照带水印
const onTakePhoto = async (item) => {
try {
const watermarkContent = $globalStore.useCommon.enterpriseName || '深圳市行政执法监督码'
const res = await takePhotoWithWaterMark({ content: watermarkContent })
await pushSdkFileToFileList(item, {
localPath: res.imageURL || res.key,
fileName: `photo-${Date.now()}.jpg`,
mimeType: 'image/jpeg',
isImage: true,
})
} catch (err) {
console.error('w6s 拍照失败', err)
showToast('拍照失败或已取消')
}
}
// 录制视频
const onRecordVideo = async (item) => {
try {
const res = await startVideoRecorder({ duration: 60, quality: 1 })
// res.info.video_path 是本地视频路径
const info = res.info || res
await pushSdkFileToFileList(item, {
localPath: info.video_path,
fileName: `video-${Date.now()}.mp4`,
mimeType: 'video/mp4',
isImage: false,
})
} catch (err) {
console.error('w6s 录制失败', err)
showToast('录制失败或已取消')
}
}
// 通用:把本地文件上传后端 → 写入 fileList
const pushSdkFileToFileList = async (item, { localPath, fileName, mimeType, isImage }) => {
if (!localPath) return
const fileObj = {
url: '',
isImage,
status: 'uploading',
message: '上传中...',
}
item.fileList.push(fileObj)
try {
const res = await uploadLocalFileToBackend({ fileURL: localPath, fileName, mimeType })
const data = res && res.data ? res.data : res
const filePath = typeof data === 'string'
? data
: (data && (data.data || data.url || data.fileUrl || data.path)) || ''
if (filePath) {
fileObj.url = toFullFileUrl(filePath)
fileObj.status = 'done'
fileObj.message = ''
} else {
fileObj.status = 'failed'
fileObj.message = '上传失败'
}
} catch (err) {
console.error('w6s 上传后端失败', err)
fileObj.status = 'failed'
fileObj.message = '上传失败'
}
}
注意:
$globalStore在该组件需确认已注入。若未注入,可改为import { $globalStore } from '@/common/globalStore'(与 http.js#L3 一致)。
待用户确认的开放问题
- PDF 上传在生产如何处理? 你强调"线上不能用相册的图片和视频,必须现场拍摄/录制"。PDF 是文档,无法现场采集。请选择:
- (A) 生产环境不支持 PDF,只允许拍照和录制视频。
- (B) 生产环境保留 PDF,用
w6s.file.chooseFiles(文件选择器)入口,作为 action-sheet 第 3 项。
- 水印文案:默认"企业名称 || '深圳市行政执法监督码'",是否需要加入其他信息(如执法人员姓名、时间)?
- 视频录制时长上限:默认 60 秒,是否需要调整(例如 30 秒 / 120 秒)?
- 视频清晰度:默认
quality: 1(一般),可选 0 高清 / 2 流畅。考虑到 200MB 上限,建议保持一般。 - 经纬度是否单独存储:
takePhotoAndAddWaterMark的locationEnable: true已把经纬度叠加到照片水印。是否还需要在item.fileList元数据中单独保存经纬度(用于后端记录)?若需要,会在onTakePhoto中额外调getLocation()并保存。 - 文件上限:现模板文案是"最多 30 个文件"但
max-count为 20,本计划沿用 20。是否需要统一?
验证方式
- 开发环境(
VITE_APP_ENV=dev):- 进入 NOS 详情材料 Tab 编辑态,确认仍显示原 van-uploader,可选图片/视频/PDF,上传后端成功。
- 生产环境(
VITE_APP_ENV=prod,beeworks 容器内):- 编辑态下点击上传按钮,弹出 action-sheet:拍照(带水印与经纬度)/ 录制视频 / 取消。
- 拍照:调起原生相机,拍完照片带时间戳+经纬度水印,自动上传后端,卡片显示缩略图。
- 录制视频:调起原生视频录制,录完自动上传后端,卡片显示视频缩略图(可点击预览)。
- 无相册/文件选择入口,确认不会出现从相册选择图片或视频的选项。
- 文件数达 20 时点击按钮提示上限。
- 检查后端
/app-enf/common/file/upload收到的file字段非空,返回 URL 正确写入item.fileList,status: 'done'。 - 预览/删除等既有功能不受影响。
涉及文件
- 新增:
src/util/w6sMedia.js - 修改:
src/views/enterprise/nos/components/NosDetailMaterialsTab.vue(模板 47-64 行 + script 新增方法) - 复用:
src/config/env.js、src/util/fileUrl.js、src/common/globalStore.js、szient-js-sdk