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

效果图

在这里插入图片描述

安装依赖

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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/226763.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL数据库,函数与分组

单行函数&#xff1a; 操作数据对象 接受参数返回一个结果 只对一行进行变换 每行返回一个结果 可以嵌套 参数也可以是一列或一个值 数值函数 基本函数&#xff1a; 注&#xff1a;ROUND(x,y)函数的y是负数时&#xff0c;即往高位进行四舍五入&#xff0c;如-3就是按百位…

机器学习 类别特征编码:Category Encoders 库的使用

✅作者简介&#xff1a;人工智能专业本科在读&#xff0c;喜欢计算机与编程&#xff0c;写博客记录自己的学习历程。 &#x1f34e;个人主页&#xff1a;小嗷犬的个人主页 &#x1f34a;个人网站&#xff1a;小嗷犬的技术小站 &#x1f96d;个人信条&#xff1a;为天地立心&…

优雅提效:Guava的字符串处理工具

第1章&#xff1a;引言 大家好&#xff0c;我是小黑&#xff0c;今天咱们要聊一聊Google Guava这个超棒的Java库&#xff0c;尤其是它的字符串处理工具。对于Java程序员来说&#xff0c;字符串处理是日常工作的一部分&#xff0c;而Guava在这方面提供了非常强大的支持。使用Gu…

12.7_黑马数据结构与算法Java

030 单向链表 get 不会提前拿到所有的索引值&#xff0c;这样维护起来非常不方便。因此&#xff0c;我们是在遍历的时候得到他的索引值 %d&#xff1a;数字的占位符就是用百分号d表示 %n&#xff1a;换行符 thinking:String.format()? String.format()的详细用法_七月J的博客-…

大数据在互联网营销中的应用:案例与策略

互联网时代的营销领域正经历着一场由大数据驱动的变革。在2023年&#xff0c;大数据的应用已成为推动市场策略和决策的关键因素。本文将探讨大数据如何影响互联网营销&#xff0c;并通过实际案例分析展示其在提升营销效果方面的作用。 首先&#xff0c;通过分析海量数据&#x…

如何配置Modbus转Profinet网关与6台232 Modbus伺服通信

Modbus转Profinet网关连接6台232 Modbus伺服同时控制的解决方案不仅简化了设备连接&#xff0c;还减少了人力投入和维护成本。通过Modbus转Profinet网关&#xff0c;操作人员只需对Profinet网关进行设置和监控&#xff0c;即可实现对6台232 Modbus伺服的集中控制。 Modbus转Pro…

中山大学李华山、王彪课题组开发 SEN 机器学习模型,高精度预测材料性能

内容一览&#xff1a;了解全局晶体对称性并分析等变信息&#xff0c;对于预测材料性能至关重要&#xff0c;但现有的、基于卷积网络的算法尚且无法完全实现这些需求。针对于此&#xff0c;中山大学的李华山、王彪课题组&#xff0c;开发了一款名为 SEN 的机器学习模型&#xff…

史上最强 Charles 抓包

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

Python中如何判断List中是否包含某个元素

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 在Python中&#xff0c;判断一个列表&#xff08;List&#xff09;是否包含某个特定元素是常见的任务之一。在本文中&#xff0c;将深入探讨多种判断List成员包含性的方法&#xff0c;并提供丰富的示例代码&…

嵌入式工程师校招经验与学习路线总结

前言&#xff1a;不知不觉2023年秋招已经结束&#xff0c;作者本人侥幸于秋招中斩获数十份大差不差的OFFER&#xff0c;包含&#xff1a;Top级的AIGC&#xff0c;工控龙头&#xff0c;国产MCU原厂&#xff0c;医疗器械&#xff0c;新能源车企等。总而言之&#xff0c;秋招总体情…

量子纠缠通讯:未来信息技术的革新者

量子纠缠通讯:未来信息技术的革新者 引言 在科技日新月异的今天,我们正逐步走进一个全新的科技时代——量子时代。量子纠缠通讯作为量子技术的重要分支,以其独特的优势和巨大的潜力,成为了当今世界研究的热点。本文将带您深入探讨量子纠缠通讯的奥秘,了解其原理、优势、…

架构师-3.用户体系

系统提供了 2 种类型的用户,分别满足对应的管理后台、用户 App 场景。 1.AdminUser 管理员用户,前端访问 s-ui-admin-vue3管理后台,后端访问 /admin-api/** RESTful API 接口。 2.MemberUser 会员用户,前端访问 s-ui-user用户 App,后端访问 /app-api/** RESTful API 接口。…

Java中lambda表达式的使用

&#x1f495;"我不要麻木的死去"&#x1f495; 作者&#xff1a;Mylvzi 文章主要内容&#xff1a;Java中lambda表达式的使用 一.背景 lambda表达式是Java SE 8中一个重要的新特性&#xff0c;允许你使用一个表达式来代替功能接口。lambda表达式可以看作一个没有返…

基于SpringBoot+Vue实现的小区物业管理系统

文章目录 系统介绍技术选型成果展示账号地址及其他说明 系统介绍 基于SpringBootVue实现的小区物业管理系统是为物业管理打造的一款在线管理平台&#xff0c;它可以实时完成信息处理&#xff0c;对小区信息、住户等进行在线管理&#xff0c;使其系统化和规范化。 系统功能说明…

架构师一1.功能权限

1. RBAC 权限模型 系统采用 RBAC 权限模型&#xff0c;全称是 Role-Based Access Control 基于角色的访问控制。 简单来说&#xff0c;每个用户拥有多个角色&#xff0c;每个角色拥有多个菜单&#xff0c;菜单中存在菜单权限、按钮权限。这样&#xff0c;就形成了 “用户<-…

画好一张规范的原理图,这些点你可要注意了!

不光是代码有可读性的说法&#xff0c;原理图也有。很多时候原理图不仅仅是给自己看的&#xff0c;也会给其它人看&#xff0c;如果可读性差&#xff0c;会带来一系列沟通问题。所以&#xff0c;要养成良好习惯&#xff0c;做个规范的原理图。此外&#xff0c;一个优秀的原理图…

msvcp140_ATOMIC_WAIT.dll丢失的相关解决方法分享

在计算机使用过程中&#xff0c;我们可能会遇到一些错误提示&#xff0c;其中之一就是msvcp140_CODECVT_IDS.dll丢失。这个错误通常会导致某些应用程序无法正常运行&#xff0c;给用户带来困扰。本文将详细介绍msvcp140_CODECVT_IDS.dll的作用和影响&#xff0c;并提供5个解决办…

【C语言】函数递归--输出n的k次方

题目描述&#xff1a; 递归实现n的k次方 代码如下&#xff1a; #include<stdio.h> int nk(int n, int k) {if (k > 0)return n * nk(n, k - 1); } int main() {int ret 0;int n 0;int k 0;scanf("%d", &n);scanf("%d", &k);ret nk(n…

基于springboot + vue的社区医院信息系统

qq&#xff08;2829419543&#xff09;获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;springboot 前端&#xff1a;采用vue技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xf…

C:算术移位和逻辑移位傻傻分不清楚

1. 算术移位与逻辑移位概念 算术移位指令对带符号数进行移位。 逻辑移位指令对无符号数进行移位。 算术左移、右移&#xff0c;逻辑左移、右移 如图所示 &#xff1a; 这里有一个进位位C&#xff0c;它就是标志寄存器 &#xff08;即状态寄存器&#xff0c;亦称程序状态字寄…