不知道大家有没有做过那种调用摄像头拍照,然后和签字板连接签名以及文件上传预览的功能,根据产品的要求把这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上调用成功