vue3 + mark.js 实现文字标注功能

这篇具有很好参考价值的文章主要介绍了vue3 + mark.js 实现文字标注功能。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

效果图

vue3 + mark.js 实现文字标注功能,WEB前端,javascript,vue.js,前端文章来源地址https://www.toymoban.com/news/detail-756419.html

安装依赖

npm install mark.js --save-dev
npm i nanoid

代码块

<template>
  <!-- 文档标注 -->
  <header>
    <el-button
      type="primary"
      :disabled="selectedTextList.length == 0 ? true : false"
      ghost
      @click="handleAllDelete"
    >
      清空标记
    </el-button>
    <el-button
      type="primary"
      :disabled="selectedTextList.length == 0 ? true : false"
      @click="handleSave"
    >
      保存
    </el-button>
  </header>
  <main>
    <div id="text-container" class="text">
      {{ markContent }}
    </div>
    <!-- 标签选择 -->
    <div
      v-if="tagInfo.visible && tagList.length > 0"
      :class="['tag-box p-4 ']"
      :style="{ top: tagInfo.top + 'px', left: tagInfo.left + 'px' }"
    >
      <div
        v-for="i in tagList"
        :key="i.tag_id"
        class="tag-name"
        @click="handleSelectLabel(i)"
      >
        <div>
          <p>{{ i.tag_name }}</p>
          <el-button
            v-if="i.tag_id == editTag.tag_id"
            text
            type="primary"
          ></el-button>
        </div>
        <div
          :class="['w-4 h-4']"
          style="width: 30px; height: 30px"
          :style="{
            background: i.tag_color,
          }"
        ></div>
      </div>
    </div>
    <!-- 重选/取消 -->
    <div
      v-if="editTag.visible"
      class="edit-tag"
      :style="{ top: editTag.top + 'px', left: editTag.left + 'px' }"
    >
      <div
        class="py-1 bg-gray-100 text-center"
        style="margin-bottom: 10px;"
        @click="handleCancel"
      >
        取 消
      </div>
      <div class="py-1 bg-gray-100 mt-2 text-center" @click="handleReset">
        重 选
      </div>
    </div>
  </main>
</template>
<script setup>
import { ref, onMounted, reactive } from 'vue'
import Mark from 'mark.js' //清空标记
import { nanoid } from 'nanoid' //一个小巧、安全、URL友好、唯一的 JavaScript 字符串 ID 生成器。

const TAG_WIDTH = 1000

const selectedTextList = ref([])

const selectedText = reactive({
  start: 0,
  end: 0,
  content: '',
})

const markContent = ref(
  '作文是经过人的思想考虑和语言组织,通过文字来表达一个主题意义的记叙方法。作文体裁包括:记叙文、说明文、应用文、议论文。作文分为小学作文、中学作文、大学作文(论文)。'
)

const tagInfo = ref({
  visible: false,
  top: 0,
  left: 0,
})

const editTag = ref({
  visible: false,
  top: 0,
  left: 0,
  mark_id: '',
  content: '',
  tag_id: '',
  start: 0,
  end: 0,
})

const tagList = [
  {
    tag_name: '1级',
    tag_color: `#DE050CFF`,
    tag_id: 'tag_id1',
  },
  {
    tag_name: '2级',
    tag_color: `#6ADE05FF`,
    tag_id: 'tag_id2',
  },
  {
    tag_name: '3级',
    tag_color: `#DE058BFF`,
    tag_id: 'tag_id3',
  },
  {
    tag_name: '4级',
    tag_color: `#9205DEFF`,
    tag_id: 'tag_id4',
  },
  {
    tag_name: '5级',
    tag_color: `#DE5F05FF`,
    tag_id: 'tag_id5',
  },
]

const handleAllDelete = () => {
  selectedTextList.value = []
  const marker = new Mark(document.getElementById('text-container'))
  marker.unmark()
}

const handleCancel = () => {
  if (!editTag.value.mark_id) return
  const markEl = new Mark(document.getElementById(editTag.value.mark_id))
  markEl.unmark()
  selectedTextList.value.splice(
    selectedTextList.value?.findIndex(t => t.mark_id == editTag.value.mark_id),
    1
  )
  tagInfo.value = {
    visible: false,
    top: 0,
    left: 0,
  }
  resetEditTag()
}

const handleReset = () => {
  editTag.value.visible = false
  tagInfo.value.visible = true
}

const handleSave = () => {
  console.log('标注的数据', selectedTextList.value)
}

const handleSelectLabel = t => {
  const { tag_color, tag_name, tag_id } = t
  tagInfo.value.visible = false
  const marker = new Mark(document.getElementById('text-container'))
  const markId = nanoid(10)
  const isReset = selectedTextList.value
    ?.map(j => j.mark_id)
    .includes(editTag.value.mark_id)
    ? 1
    : 0 // 1:重选 0:新增
  if (isReset) {
    //如若重选,则删除后再新增标签
    const markEl = new Mark(document.getElementById(editTag.value.mark_id))
    markEl.unmark()
    selectedTextList.value.splice(
      selectedTextList.value?.findIndex(
        t => t.mark_id == editTag.value.mark_id
      ),
      1
    )
  }
  marker.markRanges(
    [
      {
        start: isReset ? editTag.value.start : selectedText.start,
        length: isReset
          ? editTag.value.content.length
          : selectedText.content.length,
      },
    ],
    {
      className: 'text-selected',
      element: 'span',
      each: element => {
        element.setAttribute('id', markId)
        element.style.borderBottom = `2px solid ${t.tag_color}`
        element.style.color = t.tag_color
        element.style.userSelect = 'none'
        element.style.paddingBottom = '6px'
        element.onclick = function(e) {
          e.preventDefault()
          if (!e.target.id) return
          const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX - 300
          const item = selectedTextList.value?.find?.(
            t => t.mark_id == e.target.id
          )
          const { mark_content, tag_id, start, end } = item || {}
          editTag.value = {
            visible: true,
            top: e.offsetY + 40,
            left: e.offsetX,
            mark_id: e.target.id,
            content: mark_content || '',
            tag_id: tag_id || '',
            start: start,
            end: end,
          }
          tagInfo.value = {
            visible: false,
            top: e.offsetY + 40,
            left: left,
          }
        }
      },
    }
  )
  selectedTextList.value.push({
    tag_color,
    tag_name,
    tag_id,
    start: isReset ? editTag.value.start : selectedText.start,
    end: isReset ? editTag.value.end : selectedText.end,
    mark_content: isReset ? editTag.value.content : selectedText.content,
    mark_id: markId,
  })
}

/**
 * 获取选取的文字数据
 */
const getSelectedTextData = () => {
  const select = window?.getSelection()
  const nodeValue = select.focusNode?.nodeValue
  const anchorOffset = select.anchorOffset
  const focusOffset = select.focusOffset
  const nodeValueSatrtIndex = markContent.value?.indexOf(nodeValue)
  selectedText.content = select.toString()
  if (anchorOffset < focusOffset) {
    //从左到右标注
    selectedText.start = nodeValueSatrtIndex + anchorOffset
    selectedText.end = nodeValueSatrtIndex + focusOffset
  } else {
    //从右到左
    selectedText.start = nodeValueSatrtIndex + focusOffset
    selectedText.end = nodeValueSatrtIndex + anchorOffset
  }
}

const resetEditTag = () => {
  editTag.value = {
    visible: false,
    top: 0,
    left: 0,
    mark_id: '',
    content: '',
    tag_id: '',
    start: 0,
    end: 0,
  }
}

const drawMark = () => {
  //模拟后端返回的数据
  const res = [
    {
      start: 0, //必备
      end: 1,
      tag_color: '#DE050CFF',
      tag_id: 'tag_id1',
      tag_name: '1级',
      mark_content: '作文',
      mark_id: 'mark_id1',
    },
  ]
  selectedTextList.value = res?.map(t => ({
    tag_id: t.tag_id,
    tag_name: t.tag_name,
    tag_color: t.tag_color,
    start: t.start,
    end: t.end,
    mark_content: t.mark_content,
    mark_id: t.mark_id,
  }))
  const markList =
    selectedTextList.value?.map(j => ({
      ...j,
      start: j.start, //必备
      length: j.end - j.start + 1, //必备
    })) || []
  const marker = new Mark(document.getElementById('text-container'))
  markList?.forEach?.(function(m) {
    marker.markRanges([m], {
      element: 'span',
      className: 'text-selected',
      each: element => {
        element.setAttribute('id', m.mark_id)
        element.style.borderBottom = `2px solid ${m.tag_color}`
        element.style.color = m.tag_color
        element.style.userSelect = 'none'
        element.style.paddingBottom = '6px'
        element.onclick = function(e) {
          console.log('cccccc', m)
          const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX - 300
          editTag.value = {
            visible: true,
            top: e.offsetY + 40,
            left: e.offsetX,
            mark_id: m.mark_id,
            content: m.mark_content,
            tag_id: m.tag_id,
            start: m.start,
            end: m.end,
          }
          tagInfo.value = {
            visible: false,
            top: e.offsetY + 40,
            left: left,
          }
        }
      },
    })
  })
}

//页面初始化
onMounted(() => {
  const el = document.getElementById('text-container')
  //鼠标抬起
  el?.addEventListener('mouseup', e => {
    const text = window?.getSelection()?.toString() || ''
    if (text.length > 0) {
      const left = e.offsetX < 500 ? e.offsetX - 20 : 500
      tagInfo.value = {
        visible: true,
        top: e.offsetY + 40,
        left: left,
      }
      getSelectedTextData()
    } else {
      tagInfo.value.visible = false
    }
    //清空重选/取消数据
    resetEditTag()
  })
  //从后端获取标注数据,进行初始化标注
  drawMark()
})
</script>

<style lang="scss" scoped>
header {
  display: flex;
  // justify-content: space-between;
  align-items: center;
  padding: 0 24px;
  height: 80px;
  border-bottom: 1px solid #e5e7eb;
  user-select: none;
  background: #fff;
}

main {
  background: #fff;
  margin: 24px;
  height: 80vh;
  padding: 24px;
  overflow-y: auto;
  position: relative;
  box-shadow: 0 3px 8px 0 rgb(0 0 0 / 13%);
  .text {
    color: #333;
    font-weight: 500;
    font-size: 16px;
    line-height: 50px;
  }
  .tag-box {
    position: absolute;
    z-index: 10;
    width: 150px;
    max-height: 40vh;
    overflow-y: auto;
    background: #fff;
    border-radius: 4px;
    box-shadow: 0 9px 28px 8px rgb(0 0 0 / 3%), 0 6px 16px 4px rgb(0 0 0 / 9%),
      0 3px 6px -2px rgb(0 0 0 / 20%);
    user-select: none;
    .tag-name {
      // width: 100%;
      background: rgba(243, 244, 246, var(--tw-bg-opacity));
      font-size: 14px;
      cursor: pointer;
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 4px 8px;
      margin-top: 8px;
    }
    .tag-name:nth-of-type(1) {
      margin-top: 0;
    }
  }
  .edit-tag {
    position: absolute;
    z-index: 20;
    padding: 16px;
    cursor: pointer;
    width: 40px;
    background: #fff;
    border-radius: 4px;
    box-shadow: 0 9px 28px 8px rgb(0 0 0 / 3%), 0 6px 16px 4px rgb(0 0 0 / 9%),
      0 3px 6px -2px rgb(0 0 0 / 20%);
    user-select: none;
  }
  ::selection {
    background: rgb(51 51 51 / 20%);
  }
}
</style>

到了这里,关于vue3 + mark.js 实现文字标注功能的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • #vue3 实现前端下载excel文件模板功能

    一、需求: 前端无需通过后端接口,即可实现模板下载功能。 通过构造一个 JSON 对象,使用前端常用的 第三方库 xlsx ,可以直接将该 JSON 对象转换成 Excel 文件,让用户下载模板 二、效果: 三、源码如下:

    2024年01月19日
    浏览(64)
  • vue3+emelenui实现前端分页功能—最简单

    在一些后台管理系统或者博客管理系统中分页功能是很常见的一种服务,因为总不可能把很多数据放在一块,那样阅读起来很麻烦,所以需要分页。也是前后端中最为常见的一个功能  先看一下分页场景的模拟。  首先我们要去后端写点数据通过接口给前端: 连接数据库:

    2024年02月09日
    浏览(42)
  • 前端vue3+element plus 分页table排序功能实现

    我有如下所示的一个table,数据data是一个computed计算属性,一般情况下筛选使用element的sortable属性就可以了,可查看Element - The world\\\'s most popular Vue UI framework 但我的 table 是一个分页的 table ,当我使用 sortable 时发现,它只是对当前页的数据进行排序,但这并不是我需要的,我的

    2024年02月14日
    浏览(48)
  • 前端js如何实现截屏功能,插件推荐js-web-screen-shot

    读取dom结构转换成canvas,最后转成图片形式展示 缺点 :没有编辑功能 链接:html2canvas 大佬模仿qq截图实现的,也可以搭配webrtc实现web端远程桌面共享 链接: github gitee 简单使用 注意点:

    2024年02月06日
    浏览(45)
  • vue3+vant+cropper.js实现移动端图片裁剪功能

    最近做项目中遇到一个需求,需要对海报图片按照一定的比例进行裁剪并上传到oss。一开始这个需求思路有两个,使用canvas原生或者寻找现成的第三方库,对比了一番觉得canvas实现时间耗费较长,且秉承着不重复造轮子的原则(其实是菜)。 在进行技术调研后,决定使用vu

    2024年02月01日
    浏览(41)
  • vue3插件——vue-web-screen-shot——实现页面截图功能

    最近在看前同事发我的 vue3 框架时,发现他们有个功能是要实现页面截图功能。 最近项目遇到的要求是弹出框上传文件,需要用到页面截图,由于使用的是Vue3的框架于是选择用vue-web-screen-shot组件进行操作。(由于插件是Vue3编写的,所以只适用于Vue3的项目,如果是Vue2的项目,

    2024年02月05日
    浏览(43)
  • 使用ExcelJS实现excel的前端导出功能(Vue3+TS)

    ExcelJS :读取,操作并写入电子表格数据和样式到 XLSX 和 JSON 文件。一个 Excel 电子表格文件逆向工程项目。 github中文文档:https://github.com/exceljs/exceljs/blob/master/README_zh.md  封装excel.ts工具文件 表格页面调用excel工具文件 

    2024年02月14日
    浏览(52)
  • vue+face-api.js实现前端人脸识别功能

    近期做了一个前端vue实现人脸识别的功能,主要功能逻辑包含:人脸识别,人脸验证,唤起摄像头视频流之后从三个事件(用户点头、摇头、眨眼睛)中随机选中两个事件,待两个事件通过判断后人脸静止不动3秒钟后截取视频流生成图片,上传到阿里或者腾讯oss,通过oss返回

    2024年02月05日
    浏览(42)
  • 基于VUE3+Layui从头搭建通用后台管理系统(前端篇)二:登录界面及对应功能实现

      本章介绍系统登录界面、登录流程、登录接口等相关内容的开发,实现包括账号密码登录、短信验证登录等不同的登录方式,使用svg-capter生成图形验证码,使用expressjwt实现登录token的生成及验证。 1. 详细课程地址: https://edu.csdn.net/course/detail/38183 2. 源码下载地址: 点击

    2024年02月11日
    浏览(64)
  • vue3引入JS-SDK实现h5分享小卡片、跳转微信小程序功能

    微信js-sdk官方文档: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html 想要实现的效果: 1.登录微信公众平台,进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。 2.通过npm引入js-sdk 安装成功后,可以在package.json中找到\\\"weixin-js-sdk\\\" 3.在main.js中,将js-sdk挂载

    2024年02月11日
    浏览(62)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包