Ver código fonte

Merge branch 'feature_20250310_使用原生手机拍照,优化企业微信拍照模糊问题' into release

zhujindu 8 meses atrás
pai
commit
dbc0888e70

+ 4 - 0
.env.development

@@ -6,6 +6,10 @@ ENV = 'development'
 VUE_APP_Target=https://ssbsfatest.nipponpaint.com.cn
 VUE_APP_SSB_LINK=http://suishenbangtest.nipponpaint.com.cn
 VUE_APP_XD_LINK=http://b2btest.nipponpaint.com.cn
+# ali-oss 配置
+VUE_APP_OSS_ACCESS_KEY_ID="LTAI5tG1DTJFA16BHkzHVxjz"
+VUE_APP_OSS_ACCESS_KEY_SECRET="5brWKKEACiUsKPJeUXlB7r2iZGQHNF"
+
 # 门店拜访/开发环境
 #VUE_APP_BASE_API = '/dev-api/'
 VUE_APP_BASE_API = '/ssbsfa/'

+ 3 - 0
.env.test

@@ -12,6 +12,9 @@ VUE_APP_Target=https://ssbsfatest.nipponpaint.com.cn
 VUE_APP_SSB_LINK=http://suishenbangtest.nipponpaint.com.cn
 VUE_APP_XD_LINK=http://b2btest.nipponpaint.com.cn
 VUE_APP_BASE_API = '/ssbsfa/'
+# ali-oss 配置
+VUE_APP_OSS_ACCESS_KEY_ID="LTAI5tG1DTJFA16BHkzHVxjz"
+VUE_APP_OSS_ACCESS_KEY_SECRET="5brWKKEACiUsKPJeUXlB7r2iZGQHNF"
 
 # 正式环境
 # VUE_APP_Target=https://ssbsfa.nipponpaint.com.cn

+ 1 - 0
package.json

@@ -12,6 +12,7 @@
   },
   "dependencies": {
     "@vant/touch-emulator": "^1.4.0",
+    "ali-oss": "^6.22.0",
     "axios": "^0.24.0",
     "axios-jsonp": "^1.0.4",
     "clipboard": "^2.0.11",

+ 20 - 0
src/api/H5Camera.js

@@ -0,0 +1,20 @@
+import request from '@/utils/request';
+
+// 获取阿里云oss STS token
+export function getSTSToken(query) {
+  return request({
+    url: '/mobile/getSTSToken',
+    method: 'get',
+    params: query,
+  });
+}
+
+// 上传阿里云图片地址
+export function addH5Photo(data, signal) {
+  return request({
+    url: '/mobile/storeGroup/addH5Photo',
+    method: 'post',
+    data,
+    signal: signal,
+  });
+}

+ 9 - 0
src/api/week.js

@@ -8,3 +8,12 @@ export function selectAllocationPermission(query) {
     params: query,
   });
 }
+
+// 设置用户的拍照方式
+export function setPhotoMethod(query) {
+  return request({
+    url: '/mobile/setPhotoMethod',
+    method: 'get',
+    params: query,
+  });
+}

+ 50 - 0
src/components/H5Camera.vue

@@ -0,0 +1,50 @@
+<template>
+  <div class="h5Camera">
+    <van-icon class="photo photos" name="photograph" size="22px" color="#969696" @click="camera" />
+    <input type="file" accept="image/*" :capture="capture" id="h5Camera" />
+  </div>
+</template>
+<script>
+export default {
+  name: 'H5Camera',
+  props: {
+    capture: {
+      // 摄像头类型 camera:照相机;camcorder:摄像机;microphone:录音
+      type: String,
+      default: '',
+    },
+  },
+  data() {
+    return {
+      imgUrl: '',
+      input: null,
+    };
+  },
+  mounted() {
+    this.input = document.getElementById('h5Camera');
+    this.input.addEventListener('change', (e) => {
+      let file = e.target.files[0];
+      let reader = new FileReader();
+      reader.onload = (e) => {
+        let dataURL = e.target.result;
+        this.$emit('getImg', dataURL);
+        // console.log(dataURL);
+        // 在此处对 dataURL 进行操作,例如显示图片
+      };
+      reader.readAsDataURL(file);
+    });
+  },
+  methods: {
+    camera() {
+      if (this.input) this.input.click();
+    },
+  },
+};
+</script>
+<style lang="scss" scoped>
+.h5Camera {
+  #h5Camera {
+    display: none;
+  }
+}
+</style>

+ 136 - 33
src/components/uploadVNormal.vue

@@ -1,8 +1,16 @@
 <template>
   <div class="questionNamep">
-    <div class="cameraDiv" @click="uploadImg">
+    <!-- 0=企业微信,1=H5相机 -->
+    <!-- 企业微信拍照 -->
+    <div class="cameraDiv" @click="uploadImg" v-if="userInfo.photoMethod == '0'">
       <van-icon class="photo photos" name="photograph" size="22px" color="#969696" />
     </div>
+    <!-- 原生自带拍照 -->
+    <H5Camera
+      @getImg="getImg"
+      ref="H5Camera"
+      :capture="pictureSource == '1' ? '' : 'camera'"
+      v-else />
     <div id="allmap"></div>
     <div class="mask" v-if="progressFlag">
       <el-progress
@@ -18,7 +26,7 @@
       :imageAIVerifyFlag="imageAIVerifyFlag"
       :imageAIVerifyData="imageAIVerifyData"
       @confirmUpload="confirmUpload"
-      @uploadImgFun="uploadImg"
+      @uploadImgFun="uploadImgFun"
       :source="'visit'"
       @normalFlow="normalFlow"
       @close="close"></imageAIVerifyErr>
@@ -28,11 +36,15 @@
 <script>
 import { addstorePhoto, addVisitsPosition, addPhotoToDB } from '@/api/index';
 import imageAIVerifyErr from './imageAIVerifyErr';
+import H5Camera from '@/components/H5Camera';
 import axios from 'axios';
+import uploadAliOss from '@/utils/uploadAliOss';
+import { addH5Photo } from '@/api/H5Camera';
+import { mapState } from 'vuex';
 
 export default {
   name: 'uploadImg',
-  components: { imageAIVerifyErr },
+  components: { imageAIVerifyErr, H5Camera },
   props: {
     uploadid: {
       type: String,
@@ -111,6 +123,11 @@ export default {
       default: '',
     },
   },
+  computed: {
+    ...mapState({
+      userInfo: (state) => state.user.userInfo,
+    }),
+  },
   data() {
     return {
       shows: false,
@@ -120,12 +137,76 @@ export default {
       timeFlag: null,
       imageAIVerifyFlag: false,
       imageAIVerifyData: null, //图匠校验返回的数据
-      meidaId: '', //当前上传图片id
+      mediaId: '', //当前上传图片id
       addressesRemark: '', //当前位置信息
       controller: null, //取消请求状态
+      fileUrl: '',
     };
   },
   methods: {
+    // 原生H5拍照图片
+    // url: base64
+    getImg(base64) {
+      if (this.objectType == '' || this.objectType == null) {
+        this.$toast('请选择类型!');
+        return;
+      }
+      // 图片名称:用户名-时间戳
+      let username = localStorage.getItem('loginName');
+      let imgName = username + '-' + new Date().getTime();
+      uploadAliOss(base64, imgName)
+        .then((res) => {
+          if (res.url && res.url.indexOf('http') != -1) {
+            this.fileUrl = res.url;
+            this.uploadImagev();
+            // var form = {
+            //   fileUrl: res.url,
+            //   collectionItemId: this.collectionId,
+            //   objectType: this.objectType,
+            //   storeGroupId: this.storeGroupId,
+            //   taskId: this.taskId,
+            //   visitsId: localStorage.getItem('visitId'),
+            //   visitModel: this.visitModel,
+            //   visitSource: '1',
+            //   locationRemark: '',
+            //   parentCollectionId: this.parentCollectionId || '',
+            //   secondCollectionId: this.secondCollectionId || '',
+            //   firstCollectionId: this.firstCollectionId || '',
+            //   fourthCollectionId: this.fourthCollectionId || '',
+            //   thirdCollectionId: this.thirdCollectionId || '',
+            //   deviceCode: this.deviceCode, //设备编号
+            //   putInCode: this.putInCode, //投放编号
+            // };
+            // this.controller = null;
+            // // 需要图匠校验的添加参数和loading
+            // if (this.photoIdentifyType) {
+            //   form.photoIdentifyType = this.photoIdentifyType;
+            //   this.progress();
+            //   this.controller = new AbortController(); //取消请求
+            // } else {
+            //   this.toastLoading(0, '上传中...', true);
+            // }
+            // addH5Photo(form, this.controller ? this.controller.signal : null)
+            //   .then((res) => {
+            //     this.requestThen(res);
+            //   })
+            //   .catch((error) => {
+            //     this.requestCatch(error);
+            //   });
+          }
+        })
+        .catch((err) => {
+          console.log('err:' + err);
+        });
+    },
+    uploadImgFun() {
+      // 0=企业微信,1=H5相机
+      if (this.userInfo.photoMethod == '0') {
+        this.uploadImg();
+      } else {
+        this.$refs.H5Camera.camera();
+      }
+    },
     uploadImg() {
       var map = new TMap.Map('allmap', {
         zoom: 14,
@@ -215,7 +296,8 @@ export default {
                     localId: localIds, // 需要上传的图片的本地ID,由chooseImage接口获得
                     isShowProgressTips: 1, // 默认为1,显示进度提示
                     success: function (res) {
-                      that.uploadImagev(res.serverId, addressesRemark);
+                      that.mediaId = res.serverId;
+                      that.uploadImagev(addressesRemark);
                     },
                   });
                 },
@@ -224,7 +306,7 @@ export default {
           }
         });
     },
-    uploadImagev(meidaId, addressesRemark) {
+    uploadImagev(addressesRemark = '') {
       // 初始化重置 图匠校验
       this.resetProgress();
       this.close();
@@ -249,10 +331,10 @@ export default {
       if (that.thirdCollectionId != null && that.thirdCollectionId != 'null') {
         thirdCollectionId = that.thirdCollectionId;
       }
-      this.meidaId = meidaId;
       this.addressesRemark = addressesRemark;
       var form = {
-        mediaId: meidaId,
+        mediaId: '',
+        fileUrl: '',
         collectionItemId: that.collectionId,
         objectType: that.objectType,
         storeGroupId: that.storeGroupId,
@@ -269,6 +351,12 @@ export default {
         deviceCode: that.deviceCode, //设备编号
         putInCode: that.putInCode, //投放编号
       };
+      // 0=企业微信,1=H5相机
+      if (this.userInfo.photoMethod == '0') {
+        form.mediaId = this.mediaId; //	string	图片素材id
+      } else {
+        form.fileUrl = this.fileUrl; //	string	图片素材id
+      }
       this.controller = null;
       // 需要图匠校验的添加参数和loading
       if (this.photoIdentifyType) {
@@ -280,34 +368,42 @@ export default {
       }
       addstorePhoto(form, this.controller ? this.controller.signal : null)
         .then((res) => {
-          this.toastLoading().clear();
-          if (res.code == -1) {
-            // 图匠图片校验接口超时
-            this.requestTimeOut(res);
-          } else if (res.code == 200) {
-            // 图匠校验结果返回
-            if (this.photoIdentifyType) {
-              // 重置loaidng状态
-              this.resetProgress();
-              this.imageAIVerifyFlag = true;
-              this.imageAIVerifyData = res.data;
-            } else {
-              // 正常流程
-              this.normalFlow(res);
-            }
-          } else {
-            this.resetProgress();
-            that.$toast('上传失败!');
-          }
+          this.requestThen(res);
         })
         .catch((error) => {
-          if (error.message === 'canceled') {
-            this.$toast('取消上传');
-            console.log('请求被取消:', error.message);
-          }
-          this.resetProgress();
+          this.requestCatch(error);
         });
     },
+    // 公用请求then
+    requestThen(res) {
+      this.toastLoading().clear();
+      if (res.code == -1) {
+        // 图匠图片校验接口超时
+        this.requestTimeOut(res);
+      } else if (res.code == 200) {
+        // 图匠校验结果返回
+        if (this.photoIdentifyType) {
+          // 重置loaidng状态
+          this.resetProgress();
+          this.imageAIVerifyFlag = true;
+          this.imageAIVerifyData = res.data;
+        } else {
+          // 正常流程
+          this.normalFlow(res);
+        }
+      } else {
+        this.resetProgress();
+        that.$toast('上传失败!');
+      }
+    },
+    // 公用请求catch
+    requestCatch(error) {
+      if (error.message === 'canceled') {
+        this.$toast('取消上传');
+        console.log('请求被取消:', error.message);
+      }
+      this.resetProgress();
+    },
     // 正常流程
     normalFlow(res) {
       this.$toast('上传成功!');
@@ -342,7 +438,8 @@ export default {
     confirmUpload(res) {
       if (this.photoIdentifyType) {
         var form = {
-          mediaId: this.meidaId, //	string	图片素材id
+          mediaId: '',
+          fileUrl: '',
           visitSource: '1', //	Long	拜访模式
           storeGroupId: this.storeGroupId, //	string	门店任务组,多个用逗号隔开
           visitsId: localStorage.getItem('visitId'), //	string	拜访id
@@ -358,6 +455,12 @@ export default {
           businessId: res.data.businessId, // 当前拍摄图片id
           feedbackMessage: res.feedbackMessage,
         };
+        // 0=企业微信,1=H5相机
+        if (this.userInfo.photoMethod == '0') {
+          form.mediaId = this.mediaId; //	string	图片素材id
+        } else {
+          form.fileUrl = this.fileUrl; //	string	图片素材id
+        }
         if (res.isUpdate) {
           form.isUpdate = 'true';
         }

+ 6 - 0
src/router/index.js

@@ -436,6 +436,12 @@ const router = new VueRouter({
           meta: { title: '客资跟进' },
           component: () => import('@/views/week/assignAwait/JZfollowUp.vue'),
         },
+        {
+          path: '/systemSettings',
+          name: 'systemSettings',
+          component: () => import('@/views/week/systemSettings.vue'),
+          meta: { title: '设置' },
+        },
       ],
     },
     {

+ 22 - 0
src/utils/ali-oss.js

@@ -0,0 +1,22 @@
+const OSS = require('ali-oss');
+
+// aliyun:
+//   oss:
+//     endpoint: oss-cn-shanghai-internal.aliyuncs.com # oss对外服务的访问域名
+//     cdnpoint: cdn-svs-test.nipponpaint.com.cn
+//     accessKeyId: LTAI5tG1DTJFA16BHkzHVxjz # 访问身份验证中用到用户标识
+//     accessKeySecret: 5brWKKEACiUsKPJeUXlB7r2iZGQHNF # 用户用于加密签名字符串和oss用来验证签名字符串的密钥
+//     bucketName: svs-test
+//     publicRead: false
+const client = new OSS({
+  // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
+  accessKeyId: process.env.VUE_APP_OSS_ACCESS_KEY_ID,
+  accessKeySecret: process.env.VUE_APP_OSS_ACCESS_KEY_SECRET,
+  // yourRegion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。
+  region: 'cdn-svs-test.nipponpaint.com.cn',
+  authorizationV4: true,
+  // yourBucketName填写Bucket名称。
+  bucket: 'svs-test',
+});
+
+export default client;

+ 50 - 0
src/utils/uploadAliOss copy.js

@@ -0,0 +1,50 @@
+import client from '@/utils/ali-oss';
+
+// 自定义请求头
+const headers = {
+  // 指定Object的存储类型。
+  'x-oss-storage-class': 'Standard',
+  // 指定Object的访问权限。
+  'x-oss-object-acl': 'private',
+  // 通过文件URL访问文件时,指定以附件形式下载文件,下载后的文件名称定义为example.txt。
+  'Content-Disposition': 'attachment; filename="example.txt"',
+  // 设置Object的标签,可同时设置多个标签。
+  'x-oss-tagging': 'Tag1=1&Tag2=2',
+  // 指定PutObject操作时是否覆盖同名目标Object。此处设置为true,表示禁止覆盖同名Object。
+  'x-oss-forbid-overwrite': 'true',
+};
+
+async function uploadAliOss(data) {
+  try {
+    // 填写OSS文件完整路径和本地文件的完整路径。OSS文件完整路径中不能包含Bucket名称。
+    // 如果本地文件的完整路径中未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
+    console.log(blobToFile(base64ToBlob(data), 'h5UploadAliOss'));
+    const result = await client.put(
+      'h5UploadAliOss', //获取一个随机的文件名
+      blobToFile(base64ToBlob(data), 'h5UploadAliOss') //base64转file对象
+      // { 'Content-Type': 'image/jpeg' } //设置Content-Type
+    );
+    console.log(result);
+  } catch (e) {
+    console.log(e);
+  }
+}
+export function base64ToBlob(base64Data) {
+  let arr = base64Data.split(','),
+    fileType = arr[0].match(/:(.*?);/)[1],
+    bstr = atob(arr[1]),
+    l = bstr.length,
+    u8Arr = new Uint8Array(l);
+  while (l--) {
+    u8Arr[l] = bstr.charCodeAt(l);
+  }
+  return new Blob([u8Arr], {
+    type: fileType,
+  });
+}
+export function blobToFile(newBlob, fileName) {
+  newBlob.lastModifiedDate = new Date();
+  newBlob.name = fileName;
+  return newBlob;
+}
+export default uploadAliOss;

+ 102 - 0
src/utils/uploadAliOss.js

@@ -0,0 +1,102 @@
+const OSS = require('ali-oss');
+import { getSTSToken } from '@/api/H5Camera';
+
+// 自定义请求头
+const headers = {
+  // 指定Object的存储类型。
+  'x-oss-storage-class': 'Standard',
+  // 指定Object的访问权限。
+  'x-oss-object-acl': 'private',
+  // 通过文件URL访问文件时,指定以附件形式下载文件,下载后的文件名称定义为example.txt。
+  'Content-Disposition': 'attachment; filename="example.txt"',
+  // 设置Object的标签,可同时设置多个标签。
+  'x-oss-tagging': 'Tag1=1&Tag2=2',
+  // 指定PutObject操作时是否覆盖同名目标Object。此处设置为true,表示禁止覆盖同名Object。
+  'x-oss-forbid-overwrite': 'true',
+};
+export function dataURLtoFile(dataurl, filename) {
+  let arr = dataurl.split(','),
+    mime = arr[0].match(/:(.*?);/)[1],
+    bstr = atob(arr[1]),
+    n = bstr.length,
+    u8arr = new Uint8Array(n);
+  while (n--) {
+    u8arr[n] = bstr.charCodeAt(n);
+  }
+  return new File([u8arr], filename, { type: mime });
+}
+
+const progress = (p, _checkpoint) => {
+  // Object的上传进度。
+  console.log(p);
+  // 分片上传的断点信息。
+  console.log(_checkpoint);
+};
+
+let credentials = null;
+async function uploadAliOss(base64, filename) {
+  if (isCredentialsExpired(credentials)) {
+    // 获取 STS token.
+    const response = await getSTSToken();
+    if (response.code != 200 || !response.data) {
+      // 处理错误的HTTP状态码。
+      let err = `获取STS令牌失败: ${response.msg}`;
+      throw new Error(err);
+    }
+    credentials = await response.data;
+  }
+  const client = new OSS({
+    region: 'cn-shanghai',
+    bucket: 'svs-test',
+    accessKeyId: credentials.accessKeyId,
+    accessKeySecret: credentials.accessKeySecret,
+    stsToken: credentials.securityToken,
+    secure: true,
+    // 显式指定endpoint并修正格式
+    // 使用标准区域节点配置
+    region: 'oss-cn-shanghai',
+    // 添加网络优化参数
+    timeout: 30000,
+    retryMax: 3,
+    // 修正headers配置
+    headers: {
+      'x-oss-forbid-overwrite': 'true',
+      'Cache-Control': 'no-cache',
+    },
+  });
+  let file = dataURLtoFile(base64, filename);
+
+  const regex = /^data:image\/(\w+);base64,/; // 正则表达式匹配base64数据头部
+  const matches = base64.match(regex);
+  let fileName = filename;
+  if (matches && matches.length === 2) {
+    matches[1]; // 返回第一个捕获组,即文件扩展名
+    fileName = filename + '.' + matches[1];
+  }
+  try {
+    const result = await client.put(fileName, file);
+    console.log(result);
+    return result;
+  } catch (e) {
+    console.error('OSS Upload Error Details:', {
+      code: e.code,
+      message: e.message,
+      requestId: e.requestId,
+      hostId: e.hostId,
+    });
+    throw new Error(`文件上传失败: ${e.code} - ${e.message}`);
+  }
+}
+/**
+ * 判断临时凭证是否到期。
+ **/
+function isCredentialsExpired(credentials) {
+  if (!credentials) {
+    return true;
+  }
+  const expireDate = new Date(credentials.expiration);
+  const now = new Date();
+  // 如果有效期不足一分钟,视为过期。
+  return expireDate.getTime() - now.getTime() <= 60000;
+}
+export default uploadAliOss;

+ 2 - 1
src/views/home/index.vue

@@ -1,7 +1,8 @@
 <template>
   <div class="homePage" ref="homePage">
     <div class="content">
-      <van-nav-bar class="navBar" left-arrow title="门店拜访" @click-left="onClickLeft" />
+      <van-nav-bar class="navBar" left-arrow title="门店拜访" @click-left="onClickLeft">
+      </van-nav-bar>
       <van-tabs
         class="myTab"
         type="card"

+ 7 - 0
src/views/week/index.vue

@@ -44,6 +44,13 @@
             </template>
           </van-cell>
         </van-cell-group>
+        <van-cell-group inset class="mtb10">
+          <van-cell title="设置" is-link to="/systemSettings">
+            <template #icon>
+              <van-icon name="setting-o" class="zicon" color="#0158ba" />
+            </template>
+          </van-cell>
+        </van-cell-group>
         <!--      客资类-->
         <van-cell-group inset class="mtb10">
           <van-cell title="客资&投诉任务" to="/clew" v-if="customerClueButton">

+ 79 - 0
src/views/week/systemSettings.vue

@@ -0,0 +1,79 @@
+<template>
+  <div class="systemSettings">
+    <van-nav-bar class="navBar" title="设置" left-arrow @click-left="onClickLeft" />
+    <div class="content">
+      <div class="module">
+        <div class="title">拜访设置</div>
+        <div class="box">
+          <div class="label">拍照模糊时取原照片,但会每张保存到本地相册</div>
+          <div class="value">
+            <van-switch v-model="switchChecked" size="20" @change="change" />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import { setPhotoMethod } from '@/api/week';
+import store from '@/store';
+export default {
+  name: 'systemSettings',
+  computed: {
+    ...mapState({
+      userInfo: (state) => state.user.userInfo,
+    }),
+  },
+  data() {
+    return {
+      switchChecked: false,
+    };
+  },
+  created() {
+    // 0=企业微信,1=H5相机
+    this.switchChecked = this.userInfo.photoMethod == 0 ? false : true;
+  },
+  methods: {
+    change(value) {
+      console.log(value);
+      this.toastLoading(0, '加载中...', true);
+      setPhotoMethod({ photoMethod: value ? 1 : 0 }).then((res) => {
+        this.toastLoading().clear();
+        if (res.code == 200) {
+          this.$toast('设置成功');
+          // 获取移动端获取用户信息接口
+          store.dispatch('getUserInfo');
+        }
+      });
+    },
+    onClickLeft() {
+      this.$router.go(-1);
+    },
+  },
+};
+</script>
+<style lang="scss" scoped>
+.systemSettings {
+  width: 100%;
+  height: 100%;
+  .content {
+    margin: 10px 16px;
+    .module {
+      margin-bottom: 10px;
+      .title {
+        font-size: 16px;
+        padding: 10px 0;
+      }
+      .box {
+        background: #fff;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 10px;
+        font-size: 16px;
+      }
+    }
+  }
+}
+</style>