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

上传功能按环境拆分:本地用 van-uploader,生产强制现场拍摄/录制

Context(背景)

NosDetailMaterialsTab.vue 第 48-63 行使用单个 van-uploader 同时处理图片/视频/PDF。生产环境(beeworks 容器/深 i 企)内合规要求:线上不能用相册的图片和视频,必须是现场拍摄和录制,且拍照必须带水印与经纬度

按环境拆分上传入口:

  • 开发环境(dev):保留现有 van-uploader 不变(支持图片/视频/PDF)。
  • 生产环境(prod):只允许两种方式,均来自设备现场采集:
    1. 拍照w6s.image.takePhotoAndAddWaterMarktimeEnable: true, locationEnable: true 自动叠加时间戳与经纬度水印)
    2. 录制视频w6s.video.startVideoRecoder
    3. 上传到后端 → w6s.file.upload(原生 FileTransfer,直接传本地路径,无需 base64 转换)
    4. 经纬度获取(可选) → 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 + tokenclientid: 9e6eb24cd4e169a98b0d2cd8034d877bw6s.file.upload 需手动传入。
  • URL 拼接:复用 src/util/fileUrl.js 的 toFullFileUrl
  • 全局状态$globalStore.useCommon(取 tokenenterpriseName)。

实施步骤

步骤 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 一致)。

待用户确认的开放问题

  1. PDF 上传在生产如何处理? 你强调"线上不能用相册的图片和视频,必须现场拍摄/录制"。PDF 是文档,无法现场采集。请选择:
    • (A) 生产环境不支持 PDF,只允许拍照和录制视频。
    • (B) 生产环境保留 PDF,用 w6s.file.chooseFiles(文件选择器)入口,作为 action-sheet 第 3 项。
  2. 水印文案:默认"企业名称 || '深圳市行政执法监督码'",是否需要加入其他信息(如执法人员姓名、时间)?
  3. 视频录制时长上限:默认 60 秒,是否需要调整(例如 30 秒 / 120 秒)?
  4. 视频清晰度:默认 quality: 1(一般),可选 0 高清 / 2 流畅。考虑到 200MB 上限,建议保持一般。
  5. 经纬度是否单独存储takePhotoAndAddWaterMarklocationEnable: true 已把经纬度叠加到照片水印。是否还需要在 item.fileList 元数据中单独保存经纬度(用于后端记录)?若需要,会在 onTakePhoto 中额外调 getLocation() 并保存。
  6. 文件上限:现模板文案是"最多 30 个文件"但 max-count 为 20,本计划沿用 20。是否需要统一?

验证方式

  1. 开发环境VITE_APP_ENV=dev):
    • 进入 NOS 详情材料 Tab 编辑态,确认仍显示原 van-uploader,可选图片/视频/PDF,上传后端成功。
  2. 生产环境VITE_APP_ENV=prod,beeworks 容器内):
    • 编辑态下点击上传按钮,弹出 action-sheet:拍照(带水印与经纬度)/ 录制视频 / 取消。
    • 拍照:调起原生相机,拍完照片带时间戳+经纬度水印,自动上传后端,卡片显示缩略图。
    • 录制视频:调起原生视频录制,录完自动上传后端,卡片显示视频缩略图(可点击预览)。
    • 无相册/文件选择入口,确认不会出现从相册选择图片或视频的选项。
    • 文件数达 20 时点击按钮提示上限。
  3. 检查后端 /app-enf/common/file/upload 收到的 file 字段非空,返回 URL 正确写入 item.fileListstatus: 'done'
  4. 预览/删除等既有功能不受影响。

涉及文件

  • 新增:src/util/w6sMedia.js
  • 修改:src/views/enterprise/nos/components/NosDetailMaterialsTab.vue(模板 47-64 行 + script 新增方法)
  • 复用:src/config/env.jssrc/util/fileUrl.jssrc/common/globalStore.jsszient-js-sdk