vue canvas 绘制选定区域 矩形框

客户那边文档相当的多,目前需要协助其将文档转为数据写入数据库,并与其他系统进行数据共享及建设,所以不得不搞一个识别的功能,用户上传PDF文档后,对于关键信息点进行识别入库!

以下为核心代码,直接分享,到中午吃饭时间了,就大概分享一下。

html 复制代码
<canvas id="imgCanvas" style="border:1px solid rgb(230,230,230)"></canvas>
javascript 复制代码
initCanvas() {
    let rectArr = []
    let currAreas = []
    let canvasEle = document.getElementById('imgCanvas')
    let elRef = this.$refs.canvaxbox
    // canvasEle.width = elRef.clientWidth
    canvasEle.height = elRef.clientHeight
    canvasEle.width = (210 / 297) * elRef.clientHeight

    let ctx = canvasEle.getContext('2d')
    // 给矩形的设置颜色
    ctx.strokeStyle = '#448ef7'
    this.saveCtx = ctx

    let drawRect = (x1, y1, x2, y2) => {
        let rectWidth = Math.abs(x2 - x1)
        let rectHeight = Math.abs(y2 - y1)
        let endX = Math.min(x1, x2)
        let endY = Math.min(y1, y2)
        // 绘制之前先清空之前实时移动产生的多余的矩形路径
        ctx.clearRect(0, 0, canvasEle.width, canvasEle.height)
        ctx.strokeStyle = '#448ef7'
        // 绘制之前那些存储在 this.drawedAreas 数组中的矩形
        if (this.img) {
            ctx.drawImage(this.img, 0, 0, canvasEle.width, canvasEle.height)
        }

        currAreas = [endX, endY, rectWidth, rectHeight]
        this.drawedAreas.forEach(element => {
            ctx.beginPath();
            ctx.strokeRect(...element)
            ctx.stroke();
        });
        // 开始本次路径
        ctx.beginPath();
        // 绘制本次的矩形路径
        ctx.rect(...currAreas);
        // 开始填充矩形
        ctx.stroke();
    }

    let canvasMoveHandler = (e) => {
        drawRect(rectArr[0], rectArr[1], e.offsetX, e.offsetY)
    }

    let canvasMouseUpHandler = () => {
        this.drawedAreas.push(currAreas)
        canvasEle.removeEventListener('mousemove', canvasMoveHandler)
        canvasEle.removeEventListener('mouseup', canvasMouseUpHandler)
    }
    // 给canvas注册事件按下事件
    let canvasDownHandler = (e) => {
        if (this.toolsIndex == 1) {
            rectArr = [e.offsetX, e.offsetY]
            // 按下的时候需要注册移动事件
            canvasEle.addEventListener('mousemove', canvasMoveHandler)
            // 抬起事件
            canvasEle.addEventListener('mouseup', canvasMouseUpHandler)
        }
    }
    canvasEle.addEventListener('mousedown', canvasDownHandler)
},

以上是核心代码,绑定点击及拖动事件绘制待定区域!

页面整体代码,包含一些测试数据,我没有删除,你自己进行分析删除即可.

javascript 复制代码
<template>
    <div class="app-container" style="padding: 0px;">
        <div class="tool-box flex-row w-100">
            <el-button type="primary" icon="el-icon-upload" @click="uploadVisible = !uploadVisible">上传PDF文档</el-button>
            <el-button type="primary" icon="el-icon-upload2" @click="backOneStep">回退</el-button>
            <el-button type="danger" @click="clearAll">重置</el-button>
            <!-- <el-button size="mini" type="success" @click="savePoints">保存</el-button> -->
        </div>
        <div class="container-view flex-row">
            <div class="left-view flex-row jc-around">
                <div class="cover-view">
                    <el-scrollbar class="w-100 h-100 flex-row">
                        <div class="w-100 flex-col" style="height: auto;background-color:rgb(245,245,245)">
                            <div class="cover-item-view flex-col" v-for="(item, index) in coverList" :key="index"
                                @click="selOneItemAction(index)" v-loading="item.loading">
                                <el-image style="width: 100%; height: auto;background-color:rgb(230,230,230)"
                                    :src="item.url" fit="scale-down"
                                    :class="{ 'border-hi': index == crrentIndex }"></el-image>
                                <span v-if="index < coverList.length - 1" style="height: 5px;display:inline-block"
                                    class="w-100"></span>
                            </div>
                        </div>
                    </el-scrollbar>
                    <div v-if="coverList.length == 0" class="place-text flex-row jc-center">
                        <span class="place-span">未上传文档</span>
                    </div>
                </div>
                <div class="canvas-wrap flex-row jc-center" ref="canvaxbox"
                    v-loading="crrentIndex >= 0 && coverList[crrentIndex].loading">
                    <canvas id="imgCanvas" style="border:1px solid rgb(230,230,230)"></canvas>

                    <div class="view-tools flex-col">
                        <el-tooltip effect="dark" content="全页识别" placement="right">
                            <div class="w-100 flex-row jc-center" style="height: 50%;" @click="reconizerAction(0)">
                                <i class="el-icon-full-screen" style="font-size:20px"
                                    :class="{ 'toolsHili': toolsIndex == 0 }"></i>
                            </div>
                        </el-tooltip>
                        <el-tooltip effect="dark" content="区域识别" placement="right">
                            <div class="w-100 flex-row jc-center"
                                style="height: 50%;border-top:1px solid rgb(200,200,200)" @click="reconizerAction(1)">
                                <i class="el-icon-crop" style="font-size:20px"
                                    :class="{ 'toolsHili': toolsIndex == 1 }"></i>
                            </div>
                        </el-tooltip>
                    </div>
                </div>
            </div>
            <div class="right-view">
                <el-scrollbar class="result-view">
                    <div class="w-100 flex-col" v-for="(item, idex) in resultList">
                        <div class="card-view top-margin">
                            <div class="flex-row jc-end">
                                <i class="el-icon-close" style="font-size: 20px;padding:0px 0px 15px 0px"
                                    @click="closeItem(item)"></i>
                            </div>
                            <el-form :ref="`resultForm-${idex}`" :model="item" label-width="80px" class="w-100">
                                <el-form-item label="字段名称">
                                    <el-input class="w-100" v-model="item.name"></el-input>
                                </el-form-item>
                                <el-form-item label="字段类型">
                                    <el-select class="w-100" v-model="item.optionsValue" placeholder="请选择">
                                        <el-option v-for="btem in item.optionsList" :label="btem.label"
                                            :value="btem.value"></el-option>
                                    </el-select>
                                </el-form-item>
                            </el-form>
                            <div class="flex-row">
                                <span class="el-form-item__label" style="width:80px;">识别结果</span>
                                <span style="width:calc(100% - 100px);font-size:14px;">识别结果</span>
                            </div>
                        </div>
                    </div>
                </el-scrollbar>
            </div>
        </div>

        <!--表单组件-->
        <el-dialog append-to-body :close-on-click-modal="false" :visible.sync="uploadVisible" title="上传PDF文档"
            width="500px">
            <el-upload class="w-100" ref="upload" :limit="1" :before-upload="beforeUpload" :auto-upload="false" drag
                :headers="headers" :on-success="handleSuccess" :on-error="handleError" :action="pdfUploadApi">
                <i class="el-icon-upload"></i>
                <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
                <div class="el-upload__tip" slot="tip">只能上传txt doc pdf ppt pps xlsx xls docx文件,且不超过10M</div>
            </el-upload>
            <div slot="footer" class="dialog-footer">
                <el-button type="text" @click="uploadVisible = false">取消</el-button>
                <el-button :loading="loading" type="primary" @click="doUpload">确认</el-button>
            </div>
        </el-dialog>
    </div>
</template>

<script>
import { mapGetters } from 'vuex'
import { getToken } from '@/utils/auth'
//https://www.cnblogs.com/IwishIcould/p/18360209
export default {
    components: {},
    mixins: [],
    data() {
        return {
            id: null,
            name: '',
            headers: { 'Authorization': getToken() },
            coverList: [{
                url:'https://gd-hbimg.huaban.com/ba2dd60c93a9ef06595825d48c71e863a58f2cbe3f9f0-EQK5rk_fw1200',
                width:1200,
                height:1697
            },{
                url:'https://gd-hbimg.huaban.com/ba2dd60c93a9ef06595825d48c71e863a58f2cbe3f9f0-EQK5rk_fw1200',
                width:1200,
                height:1697
            },{
                url:'https://gd-hbimg.huaban.com/ba2dd60c93a9ef06595825d48c71e863a58f2cbe3f9f0-EQK5rk_fw1200',
                width:1200,
                height:1697
            },{
                url:'https://gd-hbimg.huaban.com/ba2dd60c93a9ef06595825d48c71e863a58f2cbe3f9f0-EQK5rk_fw1200',
                width:1200,
                height:1697
            },{
                url:'https://gd-hbimg.huaban.com/ba2dd60c93a9ef06595825d48c71e863a58f2cbe3f9f0-EQK5rk_fw1200',
                width:1200,
                height:1697
            },{
                url:'https://gd-hbimg.huaban.com/ba2dd60c93a9ef06595825d48c71e863a58f2cbe3f9f0-EQK5rk_fw1200',
                width:1200,
                height:1697
            }],
            submitData: [
                // {"polygon":{"x1":0,"y1":0,"x2":1920,"y2":0,"x3":1920,"y3":1080,"x4":0,"y4":1080}},
                { "polygon": { "x1": 700, "y1": 273, "x2": 975, "y2": 278, "x3": 1107, "y3": 368, "x4": 718, "y4": 354 } },
                { "polygon": { "x1": 49, "y1": 32, "x2": 183, "y2": 35, "x3": 181, "y3": 100, "x4": 55, "y4": 97 } },
                { "polygon": { "x1": 433, "y1": 250, "x2": 706, "y2": 253, "x3": 707, "y3": 392, "x4": 435, "y4": 393 } },
                {
                    "polygon": {
                        "x1": 45,
                        "y1": 539,
                        "x2": 193,
                        "y2": 538,
                        "x3": 192,
                        "y3": 622,
                        "x4": 41,
                        "y4": 623,
                        "x5": 42,
                        "y5": 623
                    }
                }
            ],
            resultList: [
                {
                    label: '',
                    value: '',
                    optionsValue: '',
                    optionsList: [{
                        label: '常规',
                        value: ''
                    }]
                }, {
                    label: '',
                    value: '',
                    optionsValue: '',
                    optionsList: [{
                        label: '常规',
                        value: ''
                    }]
                }, {
                    label: '',
                    value: '',
                    optionsValue: '0',
                    optionsList: [{
                        label: '常规',
                        value: '0'
                    }]
                }],
            loading: false,
            toolsIndex: 0,
            uploadVisible: false,
            // 所有的矩形信息
            drawedAreas: [],
            crrentIndex: 0
        }
    },
    computed: {
        ...mapGetters([
            'baseApi',
            'pdfUploadApi'
        ]),
        scrollWrapper() {
            return this.$refs.scrollbar.$refs.wrap
        }
    },
    created() {
    },
    mounted() {
        this.initCanvas()

        this.renderImgCanvas(0)
    },
    methods: {
        initCanvas() {
            let rectArr = []
            let currAreas = []
            let canvasEle = document.getElementById('imgCanvas')
            let elRef = this.$refs.canvaxbox
            // canvasEle.width = elRef.clientWidth
            canvasEle.height = elRef.clientHeight
            canvasEle.width = (210 / 297) * elRef.clientHeight

            let ctx = canvasEle.getContext('2d')
            // 给矩形的设置颜色
            ctx.strokeStyle = '#448ef7'
            this.saveCtx = ctx

            let drawRect = (x1, y1, x2, y2) => {
                let rectWidth = Math.abs(x2 - x1)
                let rectHeight = Math.abs(y2 - y1)
                let endX = Math.min(x1, x2)
                let endY = Math.min(y1, y2)
                // 绘制之前先清空之前实时移动产生的多余的矩形路径
                ctx.clearRect(0, 0, canvasEle.width, canvasEle.height)
                ctx.strokeStyle = '#448ef7'
                // 绘制之前那些存储在 this.drawedAreas 数组中的矩形
                if (this.img) {
                    ctx.drawImage(this.img, 0, 0, canvasEle.width, canvasEle.height)
                }

                currAreas = [endX, endY, rectWidth, rectHeight]
                this.drawedAreas.forEach(element => {
                    ctx.beginPath();
                    ctx.strokeRect(...element)
                    ctx.stroke();
                });
                // 开始本次路径
                ctx.beginPath();
                // 绘制本次的矩形路径
                ctx.rect(...currAreas);
                // 开始填充矩形
                ctx.stroke();
            }

            let canvasMoveHandler = (e) => {
                drawRect(rectArr[0], rectArr[1], e.offsetX, e.offsetY)
            }

            let canvasMouseUpHandler = () => {
                this.drawedAreas.push(currAreas)
                canvasEle.removeEventListener('mousemove', canvasMoveHandler)
                canvasEle.removeEventListener('mouseup', canvasMouseUpHandler)
            }
            // 给canvas注册事件按下事件
            let canvasDownHandler = (e) => {
                if (this.toolsIndex == 1) {
                    rectArr = [e.offsetX, e.offsetY]
                    // 按下的时候需要注册移动事件
                    canvasEle.addEventListener('mousemove', canvasMoveHandler)
                    // 抬起事件
                    canvasEle.addEventListener('mouseup', canvasMouseUpHandler)
                }
            }
            canvasEle.addEventListener('mousedown', canvasDownHandler)
        },
        // 上传文件
        doUpload() {
            this.loading = true
            this.$refs.upload.submit()
        },
        beforeUpload(file) {
            let isLt2M = true
            isLt2M = file.size / 1024 / 1024 < 100
            if (!isLt2M) {
                this.loading = false
                this.$message.error('上传文件大小不能超过 100MB!')
            }
            return isLt2M
        },
        handleSuccess(response, file, fileList) {
            this.loading = false
            this.uploadVisible = false
            this.$modal.msgSuccess('上传成功')
            this.$refs.upload.clearFiles()

            response.documents.forEach(p => {
                p.loading = true
                p.url = this.baseApi + "/" + p.url
            })
            this.coverList = response.documents
            this.renderImgCanvas(0)
        },
        // 监听上传失败
        handleError(e, file, fileList) {
            const msg = JSON.parse(e.message)
            this.$notify({
                title: msg.message,
                type: 'error',
                duration: 2500
            })
            this.loading = false
        },
        renderImgCanvas(index) {
            // 计算宽高比
            this.crrentIndex = index
            let canvasEle = document.getElementById('imgCanvas')
            let ww = canvasEle.width // 画布宽度
            let wh = canvasEle.height // 画布高度

            let e = this.coverList.objectAtIndex(index)
            let iw = e.width // 图片宽度
            let ih = e.height // 图片高度

            if (iw / ih < ww / wh) { // 以高为主
                e.ratio = ih / wh
                e.canvasHeight = wh
                e.canvasWidth = wh * iw / ih
            }
            else { // 以宽为主
                e.ratio = iw / ww
                e.canvasWidth = ww
                e.canvasHeight = ww * ih / iw
            }

            // 初始化画布大小
            canvasEle.width = e.canvasWidth
            canvasEle.height = e.canvasHeight

            e.loading = true
            // 图片加载绘制
            let img = document.createElement('img')
            img.src = e.url
            img.onload = () => {
                e.loading = false
                this.saveCtx.drawImage(img, 0, 0, e.canvasWidth, e.canvasHeight)
            }
            this.img = img
        },
        clearAll() { // 清空所有绘制区域
            let canvasEle = document.getElementById('imgCanvas')
            this.saveCtx.clearRect(0, 0, canvasEle.width, canvasEle.height);
            if (this.img) {
                this.saveCtx.drawImage(this.img, 0, 0, canvasEle.width, canvasEle.height)
            }
            this.drawedAreas = []
        },
        savePoints() { // 将画布坐标数据转换成提交数据
            let objectPoints = []
            // "object": [{"polygon": {"x1":700,"y1":273,"x2":975,"y2":278,"x3":1107,"y3":368,"x4":718,"y4":354} }]
            objectPoints = currAreas.map(area => {
                let polygon = {}
                area.forEach((point, i) => {
                    polygon[`x${i + 1}`] = Math.round(point[0] * this.ratio)
                    polygon[`y${i + 1}`] = Math.round(point[1] * this.ratio)
                })
                return {
                    "polygon": polygon
                }
            })
            this.submitData = objectPoints
            console.log('最终提交数据', objectPoints)
        },
        handleScroll(e) {
            const eventDelta = e.wheelDelta || -e.deltaY * 40
            const $scrollWrapper = this.scrollWrapper
            let scrolled = $scrollWrapper.scrollLeft + eventDelta / 4
            $scrollWrapper.scrollLeft = scrolled

            this.$emit("scrolled", scrolled)
        },
        selOneItemAction(index) {
            this.crrentIndex = index
        },
        reconizerAction(tag) {
            this.toolsIndex = tag
            if (tag == 0) {
                this.clearAll()
            }
        },
        backOneStep() {
            let canvasEle = document.getElementById('imgCanvas')
            this.saveCtx.clearRect(0, 0, canvasEle.width, canvasEle.height)
            // 绘制之前那些存储在 this.drawedAreas 数组中的矩形
            if (this.img) {
                this.saveCtx.drawImage(this.img, 0, 0, canvasEle.width, canvasEle.height)
            }
            this.drawedAreas.removeLastObject()
            this.drawedAreas.forEach(element => {
                this.saveCtx.beginPath();
                this.saveCtx.strokeRect(...element)
                this.saveCtx.stroke();
            });
        }
    }
}
</script>

<style lang="scss" scoped>
@import "./dicomStyles/aiStyle.scss";

::v-deep {
    .result-view {
        .is-horizontal {
            height: 0px;
            left: 0px;
            display: none;
        }
    }

    .view-content {
        .is-horizontal {
            display: none;
        }

        .el-scrollbar__wrap {
            overflow-x: hidden;
            margin-bottom: 0px !important;
        }

        //横向滚动
        .el-scrollbar__view {
            display: flex;
            flex-direction: column;
            justify-content: flex-start;
            align-items: center;
        }

        ::-webkit-scrollbar-thumb {
            background-color: #888;
        }

        ::-webkit-scrollbar {
            height: 8px;
        }
    }

    .el-form-item {
        margin-bottom: 10px;
    }

    .el-input__inner {
        border-radius: 0px;
    }

    .el-scrollbar__wrap {
        overflow-x: hidden;
    }


    .el-image-viewer__wrapper {
        top: 55px;
    }

    .el-image__error,
    .el-image__placeholder {
        background: none;
    }

    .el-form-item__label {
        font-weight: 500;
    }

    .el-upload {
        width: 100%;
    }

    .el-upload-dragger {
        width: 100%;
    }
}
</style>

样式文件:

css 复制代码
.app-container{
    background-color: rgb(245, 245, 245);
}

.cover-view{
    position: relative;
    width: 180px;
    height: 100%;
    background-color: white;
}

.cover-item-view{
    width: 100%;
    height: auto;
}

.tool-box {
    width: 100%;
    height: 54px;
    padding: 5px 30px;
    border-bottom: 5px solid rgb(245, 245, 245);
    background-color: white;
}


.toolsHili{
    color: #0286df;
}

.view-tools{
    position: absolute;
    left: 0px;
    top: calc(50% - 50px);
    width: 34px;
    height: 100px;
    background-color: white;
    border-top-right-radius: 10px;
    border-bottom-right-radius: 10px;
    border: 1px solid rgb(200,200,200);
    border-left: none;
}

.container-view{
    width: 100%;
    height: calc(100% - 64px);
}

.left-view{
    position: relative;
    width: 70%;
    height: 100%;
    background-color: rgb(245, 245, 245);
}


.right-view{
    width: 30%;
    height: 100%;
}

.flex-row{
    display: flex;
    flex-direction: row;
    justify-content: flex-start;
    align-items: center;
}

.jc-end{
    justify-content: flex-end;
}

.jc-center{
    justify-content: center;
    align-items: center;
}


.jc-around{
    justify-content: space-around;
}


.jc-between{
    justify-content: space-between;
}

.result-view{
    position: relative;
    width: 100%;
    height: 100%;

    background-color: rgb(245, 245, 245)
}

.flex-col{
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: center;
}


.top-margin{
    margin-top: 15px;
}


.card-view{
    width: 90%;
    background-color: white;
    padding: 15px;
    border-radius: 10px;
}


.w-100{
    width: 100%;
}

.h-100{
    height: 100%;
}


.view-content{
    width: 100%;
    height: calc(100% - 10px);
}

.canvas-wrap {
    position: relative;
    width: calc(100% - 190px);
    height: 100%;

    background-color: white;
}

.place-text{
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

.place-span
{
    font-size: 15px;
    color: #666666;
}


.border-hi{
    border: 1px solid #0286df;
}


.canvas-view{
    position: absolute;
    left: 0;
    top: 0;
}
相关推荐
一棵开花的树,枝芽无限靠近你17 分钟前
【PPTist】历史记录功能
前端·笔记·学习
licy__1 小时前
Vue 2 中 v-text 和 v-html 指令的使用详解
开发语言·前端·javascript
tester Jeffky1 小时前
深入探索 jQuery:解锁前端开发的高效工具
前端·javascript·jquery
장숙혜2 小时前
elementui进度条Progress组件
javascript·elementui·ecmascript
butterfly_onfly2 小时前
el-table 每列使用了min-width 百分比之后,解决闪屏问题
javascript·vue.js·elementui
tester Jeffky3 小时前
ECMAScrip 与 ES2015(ES6):JavaScript 现代化编程的里程碑
javascript·正则表达式·es6
海上彼尚3 小时前
前端自己也能开启HTTPS
前端·vue.js·https·vue
计算机毕业设计指导3 小时前
基于 Spring Boot + Vue 的宠物领养系统设计与实现
vue.js·spring boot·宠物
陪你去流浪_3 小时前
Vue iframe嵌套的页面实现路由缓存 实现keep-alive效果
前端·vue.js·缓存
Jing_jing_X4 小时前
心情追忆- SEO优化提升用户发现率
前端·后端·产品经理·个人开发·流量运营