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;
}
相关推荐
树叶会结冰20 分钟前
HTML语义化:当网页会说话
前端·html
冰万森25 分钟前
解决 React 项目初始化(npx create-react-app)速度慢的 7 个实用方案
前端·react.js·前端框架
牧羊人_myr38 分钟前
Ajax 技术详解
前端
浩男孩1 小时前
🍀封装个 Button 组件,使用 vitest 来测试一下
前端
蓝银草同学1 小时前
阿里 Iconfont 项目丢失?手把手教你将已引用的 SVG 图标下载到本地
前端·icon
布列瑟农的星空1 小时前
重学React —— React事件机制 vs 浏览器事件机制
前端
程序定小飞1 小时前
基于springboot的在线商城系统设计与开发
java·数据库·vue.js·spring boot·后端
一小池勺2 小时前
CommonJS
前端·面试
孙牛牛2 小时前
实战分享:一招解决嵌套依赖版本失控问题,以 undici 为例
前端