一、需求背景:
檢查業務,檢查完成後,執行人需要簽字證明檢查完成
二、實現效果:
三、技術實現
透過canvas轉換成blob物件,可以上傳到檔案服務,或者是下載另存為到本地磁碟
注意重點,canvas的樣式的寬高和dom物件寬高一定要一致才可以,否則無法在皮膚繪製線條!
<template> <el-dialog title="簽名皮膚" :close-on-click-modal="false" append-to-body :visible.sync="visible" class="JNPF-dialog JNPF-dialog_center" lock-scroll width="600px"> <canvas id="signatureCanvas" style="width: 500px; height: 300px;"></canvas> <span slot="footer" class="dialog-footer"> <el-button @click="clearCanvas"> 清 除</el-button> <el-button @click="visible = false"> 取 消</el-button> <el-button type="primary" @click="dataFormSubmit()" :loading="btnLoading"> 確 定</el-button> </span> </el-dialog> </template> <script> import request from '@/utils/request' export default { name: 'SignPanel', components: {}, props: { pathType: { type: String, require: true, default: 'annexpic' }, apiData: { type: Object, require: true, default: () => {} } }, data() { return { visible: false, btnLoading: false, canvasInstance: null, canvasContext: null, drawing: false, lastX: false, lastY: false } }, mounted() { }, methods: { init() { this.visible = true this.$nextTick(() => { this.initialCanvas() }) }, initialCanvas() { // 獲取畫布元素和上下文物件 const canvas = document.getElementById('signatureCanvas') const ctx = canvas.getContext('2d'); this.canvasInstance = canvas this.canvasContext = ctx // 設定 canvas 的寬度和高度 canvas.width = 500; // 根據需要設定 canvas.height = 300; // 根據需要設定 // // 初始化變數 this.drawing = false this.lastX = 0 this.lastY = 0 const _that = this; // 處理滑鼠按下事件 canvas.addEventListener('mousedown', (e) => { _that.lastX = e.offsetX _that.lastY = e.offsetY _that.drawing=true; }) // 處理滑鼠移動事件 canvas.addEventListener('mousemove', (e) => { if (!_that.drawing) return ctx.beginPath() ctx.moveTo(_that.lastX, _that.lastY) ctx.lineTo(e.offsetX , e.offsetY) ctx.stroke() ctx.closePath() _that.lastX = e.offsetX _that.lastY = e.offsetY }) // 處理滑鼠鬆開事件 canvas.addEventListener('mouseup', (e) => { _that.drawing = false }) // 處理滑鼠離開事件 canvas.addEventListener('mouseout', () => { _that.drawing = false }) }, dataFormSubmit() { this.toUploadSignPic() }, dataURLtoBlob(dataUrl) { const arr = dataUrl.split(','), mime = arr[0].match(/:(.*?)/)[1] const bstr = atob(arr[1]) let n = bstr.length const u8arr = new Uint8Array(n) while(n--){ u8arr[n] = bstr.charCodeAt(n) } return new Blob([u8arr], {type:mime}) }, toUploadSignPic() { const dataUrl = this.canvasInstance.toDataURL('image/png') const blobData = this.dataURLtoBlob(dataUrl) // 建立 File 物件 const fileName = 'image.png' const file = new File([blobData], fileName, {type: 'image/png'}) const formData = new FormData() formData.append('file', file) this.uploadSignApi(formData).then(res => { if (res.code !== 200) return this.$emit('whenSuccess', res, file) this.visible = false }).catch(err => { this.$emit('whenError', err, file) this.visible = false }) }, toDownloadSignPic() { const dataUrl = this.canvasInstance.toDataURL('image/png') const link = document.createElement('a') link.href = dataUrl link.download = 'signature.png' link.click() }, clearCanvas() { this.canvasContext.clearRect(0, 0, this.canvasInstance.width, this.canvasInstance.height) }, uploadSignApi(formData) { const apiPath = `/api/file/Uploader/${this.pathType}` const param = this.apiData Object.keys(param).forEach(key => ( formData.append(key, param[key]) )) return request({ url: apiPath, method: 'POST', headers: { 'Content-Type': 'multipart/form-data' }, data: formData }) } } } </script> <style scoped lang="scss"> canvas { border: 1px solid #DCDFE6; cursor: crosshair; border-radius: 5px; } </style>
因為相容現有系統的元件,我而外將框架自帶的圖片上傳改造成簽名上傳元件
圖片上傳元件點選時一定會選取本地檔案,為了解決這個問題我是選擇直接隱藏了上傳元件
改為追加了一個簽名皮膚按鈕,皮膚確認時,發射器回撥到元件上傳成功的回撥
因為簽名只存在一份,所以檔案數量限制1即可,透過上傳成功的回撥就能攔截處理
<template> <div class="UploadFile-container"> <el-button @click="openSignPanel" style="margin-right: 5px;">開啟簽名</el-button> <template v-if="fileList.length"> <transition-group class="el-upload-list el-upload-list--picture-card" tag="ul" name="el-list"> <li class="el-upload-list__item is-success" v-for="(file,index) in fileList" :key="file.fileId"> <el-image :src="define.comUrl+file.url" class="el-upload-list__item-thumbnail" :preview-src-list="getImgList(fileList)" :z-index="10000" :ref="'image'+index"> </el-image> <span class="el-upload-list__item-actions"> <span class="el-upload-list__item-preview" @click="handlePictureCardPreview(index)"> <i class="el-icon-zoom-in"></i> </span> <span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(index)"> <i class="el-icon-delete"></i> </span> </span> </li> </transition-group> </template> <template v-if="!detailed"> <el-upload v-show="false" :action="define.comUploadUrl+'/'+type" :headers="uploadHeaders" :data="params" ref="elUpload" :on-success="handleSuccess" :multiple="limit!==1" :show-file-list="false" accept="image/*" :before-upload="beforeUpload" :disabled="disabled" list-type="picture-card" :auto-upload="false" class="upload-btn"> <i slot="default" class="el-icon-plus" disabled></i> </el-upload> </template> <template> <div class="el-upload__tip" slot="tip" v-if="tipText">{{ tipText }}</div> </template> <sign-panel :visible.sync="signPaneVisible" :path-type="type" :api-data="params" ref="signForm" @whenSuccess="signUploadSuccess" @whenError="signUploadError" /> </div> </template> <script> import emitter from 'element-ui/src/mixins/emitter' import SignPanel from '@/components/Generator/components/Upload/SignPanel.vue' import BigForm from '@/views/dp-mng/scr-se-check/big-form.vue' let { methods: { dispatch } } = emitter const units = { KB: 1024, MB: 1024 * 1024, GB: 1024 * 1024 * 1024 } export default { name: 'UploadSign', components: { BigForm, SignPanel }, props: { value: { type: Array, default: () => [] }, type: { type: String, default: 'annexpic' }, disabled: { type: Boolean, default: false }, detailed: { type: Boolean, default: false }, showTip: { type: Boolean, default: false }, limit: { type: Number, default: 0 }, accept: { type: String, default: 'image/*' }, sizeUnit: { type: String, default: 'MB' }, pathType: { type: String, default: 'defaultPath' }, isAccount: { type: Number, default: 0 }, folder: { type: String, default: '' }, fileSize: { default: 10 }, tipText: { type: String, default: '' } }, data() { return { signPaneVisible: false, fileList: [], uploadHeaders: { Authorization: this.$store.getters.token }, } }, watch: { value: { immediate: true, handler(val) { this.fileList = Array.isArray(val) ? val : [] } } }, computed: { params() { return { pathType: this.pathType, isAccount: this.isAccount, folder: this.folder } } }, methods: { signUploadSuccess(result, file) { this.handleSuccess(result, file, this.$refs.elUpload.fileList) }, signUploadError() { console.log(result) }, openSignPanel() { this.signPaneVisible = true this.$refs.signForm.init() }, beforeUpload(file) { if (this.fileList.length >= this.limit) { this.handleExceed() return false } const unitNum = units[this.sizeUnit]; if (!this.fileSize) return true let isRightSize = file.size / unitNum < this.fileSize if (!isRightSize) { this.$message.error(`圖片大小超過${this.fileSize}${this.sizeUnit}`) return isRightSize; } let isAccept = new RegExp('image/*').test(file.type) if (!isAccept) { this.$message.error(`請上傳圖片`) return isAccept; } return isRightSize && isAccept; }, handleSuccess(res, file, fileList) { if (this.fileList.length >= this.limit) return this.handleExceed() if (res.code == 200) { this.fileList.push({ name: file.name, fileId: res.data.name, url: res.data.url }) this.$emit('input', this.fileList) this.$emit('change', this.fileList) dispatch.call(this, 'ElFormItem', 'el.form.change', this.fileList) } else { this.$refs.elUpload.uploadFiles.splice(fileList.length - 1, 1) fileList.filter(o => o.uid != file.uid) this.$emit('input', this.fileList) this.$emit('change', this.fileList) dispatch.call(this, 'ElFormItem', 'el.form.change', this.fileList) this.$message({ message: res.msg, type: 'error', duration: 1500 }) } }, handleExceed(files, fileList) { this.$message.warning(`當前限制最多可以上傳${this.limit}張圖片`) }, handlePictureCardPreview(index) { this.$refs['image' + index][0].clickHandler() }, handleRemove(index) { this.fileList.splice(index, 1) this.$refs.elUpload.uploadFiles.splice(index, 1) this.$emit("input", this.fileList) this.$emit('change', this.fileList) dispatch.call(this, 'ElFormItem', 'el.form.change', this.fileList) }, getImgList(list) { const newList = list.map(o => this.define.comUrl + o.url) return newList } } } </script> <style lang="scss" scoped> >>> .el-upload-list--picture-card .el-upload-list__item { width: 120px; height: 120px; } >>> .el-upload--picture-card { width: 120px; height: 120px; line-height: 120px; } .upload-btn { display: inline-block; } .el-upload__tip { color: #a5a5a5; word-break: break-all; line-height: 1.3; margin-top: 5px; } // .el-upload-list--picture-card { // display: inline-block; // height: 0; // } </style>
表單效果: