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;
}
相关推荐
糕冷小美n1 小时前
elementuivue2表格不覆盖整个表格添加固定属性
前端·javascript·elementui
小哥不太逍遥1 小时前
Technical Report 2024
java·服务器·前端
沐墨染1 小时前
黑词分析与可疑对话挖掘组件的设计与实现
前端·elementui·数据挖掘·数据分析·vue·visual studio code
anOnion1 小时前
构建无障碍组件之Disclosure Pattern
前端·html·交互设计
threerocks2 小时前
前端将死,Agent 永生
前端·人工智能·ai编程
问道飞鱼2 小时前
【前端知识】Vite用法从入门到实战
前端·vite·项目构建
爱上妖精的尾巴2 小时前
8-10 WPS JSA 正则表达式:贪婪匹配
服务器·前端·javascript·正则表达式·wps·jsa
Zhencode3 小时前
Vue3响应式原理之ref篇
vue.js
shadow fish3 小时前
react学习记录(三)
javascript·学习·react.js
小疙瘩4 小时前
element-ui 中 el-upload 多文件一次性上传的实现
javascript·vue.js·ui