diff --git a/zdxt-web-client/zdxt-efcode-enterprise-app/.trae/documents/upload-sdk-split-env.md b/zdxt-web-client/zdxt-efcode-enterprise-app/.trae/documents/upload-sdk-split-env.md
new file mode 100644
index 00000000..ce3956b5
--- /dev/null
+++ b/zdxt-web-client/zdxt-efcode-enterprise-app/.trae/documents/upload-sdk-split-env.md
@@ -0,0 +1,291 @@
+# 上传功能按环境拆分:本地用 van-uploader,生产强制现场拍摄/录制
+
+## Context(背景)
+
+[NosDetailMaterialsTab.vue](file:///d:/Enforcementcode/EnforcementCode-2.0/zdxt-web-client/zdxt-efcode-enterprise-app/src/views/enterprise/nos/components/NosDetailMaterialsTab.vue) 第 48-63 行使用单个 `van-uploader` 同时处理图片/视频/PDF。生产环境(beeworks 容器/深 i 企)内合规要求:**线上不能用相册的图片和视频,必须是现场拍摄和录制**,且**拍照必须带水印与经纬度**。
+
+按环境拆分上传入口:
+
+- **开发环境(dev)**:保留现有 `van-uploader` 不变(支持图片/视频/PDF)。
+- **生产环境(prod)**:只允许两种方式,均来自设备现场采集:
+ 1. **拍照** → [`w6s.image.takePhotoAndAddWaterMark`](https://open.beeworks.cn/js-sdk/image.html#%E5%9B%BE%E7%89%87%E6%B7%BB%E5%8A%A0%E6%B0%B4%E5%8D%B0)(`timeEnable: true, locationEnable: true` 自动叠加时间戳与经纬度水印)
+ 2. **录制视频** → [`w6s.video.startVideoRecoder`](https://open.beeworks.cn/js-sdk/video.html#%E8%A7%86%E9%A2%91%E5%BD%95%E5%88%B6)
+ 3. 上传到后端 → [`w6s.file.upload`](https://open.beeworks.cn/js-sdk/file.html#%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0)(原生 FileTransfer,直接传本地路径,无需 base64 转换)
+ 4. 经纬度获取(可选) → [`w6s.location.getLocation`](https://open.beeworks.cn/js-sdk/location.html)
+
+> **关键约束**:生产环境**不提供**相册选择、文件选择器入口,避免用户上传已有图片/视频。PDF 上传在生产环境是否保留待用户确认(见开放问题 1)。
+
+## 关键复用点
+
+- **环境判断**:[src/config/env.js](file:///d:/Enforcementcode/EnforcementCode-2.0/zdxt-web-client/zdxt-efcode-enterprise-app/src/config/env.js) 的 `appEnv`,生产值为 `'prod'`。
+- **w6s SDK 导入**:`import * as w6s from 'szient-js-sdk'`(参考 [qrCode.vue#L42](file:///d:/Enforcementcode/EnforcementCode-2.0/zdxt-web-client/zdxt-efcode-enterprise-app/src/views/enterprise/qr/qrCode.vue#L42))。鉴权已在 [enterpriseHome.vue#L295-L344](file:///d:/Enforcementcode/EnforcementCode-2.0/zdxt-web-client/zdxt-efcode-enterprise-app/src/views/enterprise/home/enterpriseHome.vue#L295-L344) 完成。
+- **后端上传地址**:[src/util/fileUpload.js](file:///d:/Enforcementcode/EnforcementCode-2.0/zdxt-web-client/zdxt-efcode-enterprise-app/src/util/fileUpload.js) 中 `/app-enf/common/file/upload`,完整 URL 为 `${baseUrl}/app-enf/common/file/upload`。
+- **认证头**:[src/http/http.js#L26-L31](file:///d:/Enforcementcode/EnforcementCode-2.0/zdxt-web-client/zdxt-efcode-enterprise-app/src/http/http.js#L26-L31) 拦截器加 `Authorization: Bearer + token` 与 `clientid: 9e6eb24cd4e169a98b0d2cd8034d877b`,`w6s.file.upload` 需手动传入。
+- **URL 拼接**:复用 [src/util/fileUrl.js](file:///d:/Enforcementcode/EnforcementCode-2.0/zdxt-web-client/zdxt-efcode-enterprise-app/src/util/fileUrl.js) 的 `toFullFileUrl`。
+- **全局状态**:`$globalStore.useCommon`(取 `token`、`enterpriseName`)。
+
+## 实施步骤
+
+### 步骤 1:新建 `src/util/w6sMedia.js`
+
+Promise 化封装 4 个方法:
+
+```js
+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 新增引入
+
+```js
+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 行)
+
+```html
+
+
+
+
+```
+
+#### 2.3 脚本逻辑
+
+```js
+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](file:///d:/Enforcementcode/EnforcementCode-2.0/zdxt-web-client/zdxt-efcode-enterprise-app/src/http/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. **经纬度是否单独存储**:`takePhotoAndAddWaterMark` 的 `locationEnable: 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.fileList`,`status: 'done'`。
+4. 预览/删除等既有功能不受影响。
+
+## 涉及文件
+
+- 新增:`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`
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
new file mode 100644
index 00000000..f1a393a8
--- /dev/null
+++ b/zdxt-web-client/zdxt-efcode-enterprise-app/src/util/w6sMedia.js
@@ -0,0 +1,83 @@
+import * as w6s from 'szient-js-sdk'
+import { baseUrl } from '@/config/env'
+import { $globalStore } from '@/common/globalStore'
+
+// 拍照并加水印(时间戳 + 经纬度自动叠加到照片)
+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,
+ })
+})
+
+// 录制视频
+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,
+ })
+})
+
+// 获取定位(经纬度)
+export const getLocation = () => new Promise((resolve, reject) => {
+ w6s.location.getLocation({ success: resolve, fail: reject })
+})
+
+// 上传本地文件到后端 /app-enf/common/file/upload
+// 使用 w6s.file.upload(Cordova FileTransfer),直接传本地路径,无需 base64 转换
+export const uploadLocalFileToBackend = ({
+ fileURL,
+ 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,
+ })
+})
diff --git a/zdxt-web-client/zdxt-efcode-enterprise-app/src/views/enterprise/nos/components/NosDetailMaterialsTab.vue b/zdxt-web-client/zdxt-efcode-enterprise-app/src/views/enterprise/nos/components/NosDetailMaterialsTab.vue
index 94539639..ea3791ff 100644
--- a/zdxt-web-client/zdxt-efcode-enterprise-app/src/views/enterprise/nos/components/NosDetailMaterialsTab.vue
+++ b/zdxt-web-client/zdxt-efcode-enterprise-app/src/views/enterprise/nos/components/NosDetailMaterialsTab.vue
@@ -45,7 +45,9 @@
@@ -168,6 +175,15 @@
+
+
+
@@ -178,6 +194,13 @@ import noDataImg from '@/assets/images/nos/no-data.png'
import { uploadFile } from '@/util/fileUpload'
import { toFullFileUrl } from '@/util/fileUrl'
import { nosTaskCommentAPi } from '@/api/nos/index'
+import { appEnv } from '@/config/env'
+import { $globalStore } from '@/common/globalStore'
+import {
+ takePhotoWithWaterMark,
+ startVideoRecorder,
+ uploadLocalFileToBackend,
+} from '@/util/w6sMedia'
import 'vant/es/toast/style';
import veryGoodImg from '@/assets/images/eval-verygood.png'
import veryGoodDisImg from '@/assets/images/eval-verygood-dis.png'
@@ -295,6 +318,15 @@ const minDate = new Date()
const showVideoPreview = ref(false)
const previewVideoUrl = ref('')
+// ========== 上传入口(生产环境用 w6s SDK) ==========
+const isProd = computed(() => appEnv === 'prod')
+const showUploadAction = ref(false)
+const activeUploadItem = ref(null)
+const uploadActions = [
+ { name: '拍照(带水印与经纬度)' },
+ { name: '录制视频' },
+]
+
const pickerDate = ref([String(minDate.getFullYear()), String(minDate.getMonth() + 1).padStart(2, '0'), String(minDate.getDate()).padStart(2, '0')])
const onOversize = () => {
@@ -356,6 +388,88 @@ const onAfterReadImage = (file) => {
})
}
+// ========== 生产环境:w6s SDK 上传(仅现场拍摄/录制) ==========
+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 })
+ 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('录制失败或已取消')
+ }
+}
+
+// 通用:把 SDK 返回的本地文件上传后端 → 写入 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 = '上传失败'
+ }
+}
+
watch(
() => taskItemList.value,
(newList) => {
diff --git a/zdxt-web-client/zdxt-efcode-enterprise-app/vite-env/.env.enterprise-dev-wmy b/zdxt-web-client/zdxt-efcode-enterprise-app/vite-env/.env.enterprise-dev-wmy
index 8607253f..b38d1b25 100644
--- a/zdxt-web-client/zdxt-efcode-enterprise-app/vite-env/.env.enterprise-dev-wmy
+++ b/zdxt-web-client/zdxt-efcode-enterprise-app/vite-env/.env.enterprise-dev-wmy
@@ -2,7 +2,8 @@
VITE_PORT = 12001
# VITE_PROXY_TARGET = 'http://139.186.148.21:8080'
# VITE_PROXY_TARGET = 'http://172.16.21.213:8080'
-VITE_PROXY_TARGET = 'http://172.16.21.93:8080'
+# VITE_PROXY_TARGET = 'http://172.16.21.93:8080'
+VITE_PROXY_TARGET = 'http://172.16.20.72:8080'
VITE_BASE_URL = '/efcode20-ent-app/api'
VITE_CONTEXT_PATH = '/efcode20-ent-app/'
VITE_DEFAULT_LOGIN = '/enterprise/loginByPassword'