拍照和文件上传和硬件签名

93 阅读1分钟

不知道大家有没有做过那种调用摄像头拍照,然后和签字板连接签名以及文件上传预览的功能,根据产品的要求把这4个功能都封到一起了.我变成一个缝合大师了,真是太好了

<template>
  <div class="camera_outer">
    <div>
      <HeaderTitle title="底单存档"></HeaderTitle>
      <div :style="{
        height: width + 'px',
        width: height + 'px',
        display: 'flex',
        'align-items': 'center',
        'justify-content': 'center',
      }" class="camera-area">
        <video :id="highCameraId" :style="{
          'transform-origin': 'center',
          transform: `rotate(180deg)`,
          height: width + 'px',
          width: height + 'px',
          objectFit: 'cover',
        }" autoplay></video>
        <canvas style="display: none" :id="canvasCameraId" :width="videoWidth" :height="videoWidth"></canvas>
        <canvas style="display: none" id="canvasCameraId2" :width="videoHeight" :height="videoWidth"></canvas>
      </div>
      <div style="margin-top: 20px">
        <a-button style="margin-right: 20px; padding: 0 20px" type="primary" ghost @click="setImage()">拍照</a-button>
        <a-button @click="stopNavigator()" type="primary" danger>关闭高拍仪</a-button>
      </div>
    </div>
    <div style="height: 600px; flex: 1; overflow: auto; padding: 0 25px">
      <div v-if="processCfg.isSignature && !registrationRecords">
        <HeaderTitle title="电子签名"></HeaderTitle>
        <div class="autograph">
          <img style="position: absolute; height: 100%; left: 0; right: 0; top: 0; bottom: 0; margin: auto"
            :src="signature" alt="" />
          签名区
        </div>
        <div style="margin: 15px 0 70px 0">
          <a-button style="margin-right: 10px; float: left" type="primary" ghost @click="connect">签名</a-button>
        </div>
      </div>
      <HeaderTitle title="存档文件">
        <span style="margin-right: 10px; float: right">
          <a-upload v-show="true" v-model:file-list="fileList" name="file" :headers="headers"
            :customRequest="customRequest">
            <a-button style="color: #0960bd" class="bon" v-if="processCfg.isArchive"> 上传附件 </a-button>
          </a-upload>
        </span>
      </HeaderTitle>
      <div style="height: 250px; overflow: auto">
        <p v-for="(item, index) in record.refreshDataList" :key="item.name">
          <span :style="{ cursor: 'pointer', color: '#1296db' }" @click="previewImg(index)">{{ item.name }}</span>
          <svg @click="deleteImg(index)" style="cursor: pointer" t="1681117743816" class="icon" viewBox="0 0 1024 1024"
            version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2701" width="23" height="23">
            <path
              d="M202.666667 256h-42.666667a32 32 0 0 1 0-64h704a32 32 0 0 1 0 64H266.666667v565.333333a53.333333 53.333333 0 0 0 53.333333 53.333334h384a53.333333 53.333333 0 0 0 53.333333-53.333334V352a32 32 0 0 1 64 0v469.333333c0 64.8-52.533333 117.333333-117.333333 117.333334H320c-64.8 0-117.333333-52.533333-117.333333-117.333334V256z m224-106.666667a32 32 0 0 1 0-64h170.666666a32 32 0 0 1 0 64H426.666667z m-32 288a32 32 0 0 1 64 0v256a32 32 0 0 1-64 0V437.333333z m170.666666 0a32 32 0 0 1 64 0v256a32 32 0 0 1-64 0V437.333333z"
              fill="#1296db" p-id="2702"></path>
          </svg>
        </p>
        <a-image :width="200" :style="{ display: 'none' }" :preview="{
          visible,
          onVisibleChange: setVisible,
        }" :src="preview" />
      </div>
    </div>
  </div>
  <div class="footer" v-if="!registrationRecords">
    <a-button style="margin-right: 15px" @click="onclose">关闭</a-button>
    <a-button v-if="attachments.length == 0" type="primary" @click="attachment">保存</a-button>
    <a-button v-else type="primary" @click="revise">修改</a-button>
  </div>
  <Modal title="提示" v-model:visible="reviseModal" style="text-align: center" @on-ok="ok">
    <p>请输入修改说明 :</p>
    <a-textarea v-model:value="reviseInput" placeholder="" :auto-size="{ minRows: 5, maxRows: 10 }" />
    <template #footer>
      <a-button type="default" size="large" @click="cancel">取消</a-button>
      <a-button type="primary" size="large" @click="ok">确认</a-button>
    </template>
  </Modal>
</template>
<script setup lang="tsx">
import { PluginNSV } from '@/libs/js-NSV';
import HeaderTitle from '@/components/headerTitle.vue';
import Compressor from 'compressorjs';
import { reactive, computed, onMounted, watch, ref, defineExpose, onBeforeUnmount } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { defineEmits, nextTick } from 'vue';
import { useRouter } from 'vue-router';
const props = defineProps({
  height: {
    // 这里是只指展示出来的高度,因为要旋转,所以实际上是组件的宽度
    type: Number,
    default: document.body.clientHeight - 540,
  },
  billId: String,
  billType: Number, // 附件所属单子的类型:3核销单单、4盘点单
  highCameraId: {
    type: String,
    default: '',
  },
  canvasCameraId: {
    type: String,
    default: '',
  },
  attachments: {
    type: Array,
    default: () => [],
  },
  processCfg: {
    type: Object,
    default: {
      isArchive: true, //是否开启底单存档
      isCapture: true, //是否开启高拍仪
      isSignature: true, //是否开启电子签名
    },
  },
  routerRecord: {
    type: Boolean,
    default: false,
  },
  registrationRecords: {
    type: Boolean,
    default: false,
  },
});
//签字板模块
const signature = ref();
// 改变签字板背景颜色和画笔颜色
const setBKColor = () => {
  //背景板颜色
  plugin.setBKColor(255, 255, 255, (state) => {
    console.log(state);
  });
  //画笔颜色
  plugin.setPenColor(0, 0, 0, (state) => {
    console.log(state);
  });
};
//签字板连接
let plugin: any = null;
const createTime: any = ref('');
const connect = () => {
  if (!plugin) {
    plugin = new PluginNSV();
  }
  plugin.InitPlugin(function (state) {
    if (state === 1) {
      console.log('连接成功');
      plugin.setDisplayMapMode(1, null);
      beginSign();
    } else {
      return message.warning('签字板连接失败,请检查设备连接');
    }
  });
  plugin.onConfirm = function () {
    if (!!plugin) {
      var format = 1;
      /*0-jpg,1-png,2-gif,3-bmp*/
      var w = 580,
        h = 240;
      var quality = 100;
      plugin.saveImageToBase64(format, w, h, quality, function (state, args) {
        if (state) {
          var img_base64_data = args[0];
          var img_base64 = 'data:image/png;base64,' + img_base64_data;
          signature.value = img_base64;
          record.refreshDataList.forEach((item, index) => {
            if (item.name == '签名文件.png') {
              deleteImg(index);
            }
          });
          updataImg(img_base64);
          plugin.endSign(function (state, args) {
            plugin.clearSign(function (state, args) { });
          });
        } else {
          message.warning('签名生成失败');
        }
      });
    }
  };
};
const updataImg = (src) => {
  let file = dataURLtoFile(src, new Date().getTime() + '');
  new Compressor(file, {
    uality: 0.6,
    success(result) {
      const formData = new FormData();
      formData.append('file', result, '签名文件.png'); // 通过XMLHttpRequest服务发送压缩的图像文件-Send the compressed image file to server with XMLHttpRequest.
      接口(formData)
        .then((res) => {
          record.loading = false;
          record.refreshDataList.push({
            img: src,
            id: res.result.id,
            name: res.result.name,
            type: 1,
          });
        })
        .catch((err) => {
          record.loading = false;
          console.log(err);
        });
    },
    error(e) {
      console.log(e.message);
    },
  });
};
//签字板打开
const beginSign = () => {
  if (!!plugin) {
    plugin.beginSign(function (state, args) {
      if (state) {
        plugin.moveSignWindow(660, 430, 600, 270, null);
      } else {
        message.warning(`签字板窗口打开失败.失败原因${args[0]}`);
      }
    });
  }
};
//毁灭钩子
onBeforeUnmount(() => {
  console.log('我毁灭了');
  plugin && plugin.DestroyPlugin();
});
//底单存档模块
const reviseModal = ref(false);
const reviseInput = ref('');
const ok = async () => {
  
};
const cancel = () => {
  reviseModal.value = false;
  reviseInput.value = '';
};
const router = useRouter();
const emit = defineEmits(['uploadSuccess']);
const onclose = () => {
  record.loading = false;
  emit('uploadSuccess');
  stopNavigator();
};
// 上传附件
const headers = {
  authorization: 'authorization-text',
};
const fileList: any = ref([]);
// 文件上传点击事件
const customRequest = async (e) => {
  if (fileList.value.length === 0) {
    message.error(`文件上传失败`);
  } else {
    let data = new FormData();
    data.append('file', e.file); //如果还有其他参数复制就行了
    try {
      let { result } = await 接口(data);
      record.refreshDataList.push({
        id: result.id,
        name: result.name,
        type: 2, //1是图片 2是其他类型文件 3是签名文件
      });
      fileList.value = [];
    } catch (error) {
      message.error(`文件上传失败`);
      fileList.value = [];
      return;
    }
  }
};
// 预览图片显示
const visible = ref(false);
const preview = ref('');
const setVisible = (value): void => {
  if (visible.value != value) {
    preview.value = '';
    visible.value = value;
  }
};
const previewImg = (index) => {

};

const videoWidth = 3840; // 实际摄像头的分辨率
const videoHeight = 2880; // 实际摄像头的分辨率
const record: any = reactive({
  imgSrc: '',
  thisCancas: null,
  activeClass: -1,
  thisContext: null,
  thisVideo: null,
  loading: false,
  // 图片列表
  refreshDataList: [],
});

const width = computed((): any => (props.height * 3840) / 3200);
onMounted(() => {
  record.refreshDataList = [];
  getCompetence();
  if (props.attachments.length > 0) {
    props.attachments.forEach((item) => {
      record.refreshDataList.push({
        name: item.name,
        id: item.id,
        type: item.type,
      });
    });
  }
});
// 调用权限(打开摄像头功能)
const getCompetence = () => {
  record.thisCancas = document.getElementById(props.canvasCameraId);
  record.thisContext = record.thisCancas.getContext('2d');
  record.thisVideo = document.getElementById(props.highCameraId);
  // 旧版本浏览器可能根本不支持mediaDevices,我们首先设置一个空对象
  if (navigator.mediaDevices === undefined) {
    navigator.mediaDevices = {};
  }
  // 一些浏览器实现了部分mediaDevices,我们不能只分配一个对象
  // 使用getUserMedia,因为它会覆盖现有的属性。
  // 这里,如果缺少getUserMedia属性,就添加它。
  if (navigator.mediaDevices.getUserMedia === undefined) {
    navigator.mediaDevices.getUserMedia = function (constraints) {
      // 首先获取现存的getUserMedia(如果存在)
      var getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.getUserMedia;
      // 有些浏览器不支持,会返回错误信息
      // 保持接口一致
      if (!getUserMedia) {
        return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
      }
      // 否则,使用Promise将调用包装到旧的navigator.getUserMedia
      return new Promise(function (resolve, reject) {
        getUserMedia.call(navigator, constraints, resolve, reject);
      });
    };
  }
  var constraints = { audio: false, video: true };
  navigator.mediaDevices
    .getUserMedia(constraints)
    .then(function (stream) {
      // 旧的浏览器可能没有srcObject
      if ('srcObject' in record.thisVideo) {
        record.thisVideo.srcObject = stream;
      } else {
        // 避免在新的浏览器中使用它,因为它正在被弃用。
        record.thisVideo.src = window.URL.createObjectURL(stream);
      }
      record.thisVideo.onloadedmetadata = function (e) {
        record.thisVideo.play();
      };
    })
    .catch((err) => {
      console.log(err);
    });
};
// 关闭高拍仪
const stopNavigator = () => {
   record.thisVideo.srcObject.getTracks()[0].stop();
};
//  绘制图片(拍照功能)
const setImage = () => {
  record.loading = true;
  // 点击,canvas画图
  record.thisContext.save();
  record.thisContext.clearRect(0, 0, record.thisCancas.width, record.thisCancas.height);
  record.thisContext.translate(0, record.thisCancas.height);
  record.thisContext.rotate((-90 * Math.PI) / 180);
  record.thisContext.translate(250, 3500);
  record.thisContext.rotate((-90 * Math.PI) / 180);
  record.thisContext.drawImage(record.thisVideo, 0, 0, videoWidth, videoHeight);
  record.thisContext.restore();
  // 尺寸转换
  const c2 = document.getElementById('canvasCameraId2');
  const c2d = c2.getContext('2d');
  c2d.drawImage(record.thisCancas, 0, 0, videoHeight, videoWidth, 0, 0, videoHeight, videoWidth);
  let image = c2.toDataURL('image/png');
  record.imgSrc = image;
  let file = dataURLtoFile(image, new Date().getTime() + '');
  new Compressor(file, {
    uality: 0.6,
    success(result) {
      const formData = new FormData();
      formData.append('file', result, result.name + '.jpeg'); // 通过XMLHttpRequest服务发送压缩的图像文件-Send the compressed image file to server with XMLHttpRequest.
      接口(formData)
        .then((res) => {
          record.loading = false;
          record.refreshDataList.push({
            img: image,
            id: res.result.id,
            name: res.result.name,
            type: 1,
          });
        })
        .catch((err) => {
          record.loading = false;
          console.log(err);
        });
    },
    error(e) {
      console.log(e.message);
    },
  });
};
// base64转文件
const dataURLtoFile = (dataurl, filename) => {
  var arr = dataurl.split(',');
  var mime = arr[0].match(/:(.*?);/)[1];
  var bstr = atob(arr[1]);
  var n = bstr.length;
  var u8arr = new Uint8Array(n);
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
  }
  return new File([u8arr], filename, { type: mime });
};
// 删除图片
const deleteImg = (index) => {
  record.refreshDataList.splice(index, 1);
};
// 上传附件
const attachment = async () => {
  
};
//修改
const revise = () => {
  
};
watch(
  () => props.billId,
  () => {
    record.refreshDataList = [];
  },
);
defineExpose({
  record,
});
</script>
<style lang="less" scoped>
.actived {
  display: none;
}

.autograph {
  border: 1px solid #ccc;
  height: 200px;
  line-height: 200px;
  text-align: center;
  color: #ccc;
  position: relative;
}

.footer {
  position: absolute;
  bottom: 0;
  height: 50px;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
}

p {
  margin-bottom: 3px;
  font-size: 16px;
  display: flex;
  align-content: center;
  margin-left: 5px;

  span {
    margin-right: 10px;
  }
}

/deep/.ant-upload.ant-upload-select {
  border: 1px solid #0960bd;
}

.ant-btn-dangerous.ant-btn-primary {
  background-color: #f05f6e;
}

.camera_outer {
  overflow-x: hidden;
  overflow-y: hidden;
  display: flex;
  width: 100%;
  position: relative;

  .camera-area {
    border: 1px #ccc solid;
  }
}
</style>

签字板需要自己去找厂家要接口文档,代码有点凌乱.另外调用摄像头的功能只能在本地或者https上调用成功