当前位置: 首页 > news >正文

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

效果图

在这里插入图片描述

安装依赖

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

代码块

<template><!-- 文档标注 --><header><el-buttontype="primary":disabled="selectedTextList.length == 0 ? true : false"ghost@click="handleAllDelete">清空标记</el-button><el-buttontype="primary":disabled="selectedTextList.length == 0 ? true : false"@click="handleSave">保存</el-button></header><main><div id="text-container" class="text">{{ markContent }}</div><!-- 标签选择 --><divv-if="tagInfo.visible && tagList.length > 0":class="['tag-box p-4 ']":style="{ top: tagInfo.top + 'px', left: tagInfo.left + 'px' }"><divv-for="i in tagList":key="i.tag_id"class="tag-name"@click="handleSelectLabel(i)"><div><p>{{ i.tag_name }}</p><el-buttonv-if="i.tag_id == editTag.tag_id"texttype="primary"></el-button></div><div:class="['w-4 h-4']"style="width: 30px; height: 30px":style="{background: i.tag_color,}"></div></div></div><!-- 重选/取消 --><divv-if="editTag.visible"class="edit-tag":style="{ top: editTag.top + 'px', left: editTag.left + 'px' }"><divclass="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 = 1000const 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) returnconst 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 = falsetagInfo.value.visible = true
}const handleSave = () => {console.log('标注的数据', selectedTextList.value)
}const handleSelectLabel = t => {const { tag_color, tag_name, tag_id } = ttagInfo.value.visible = falseconst 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_colorelement.style.userSelect = 'none'element.style.paddingBottom = '6px'element.onclick = function(e) {e.preventDefault()if (!e.target.id) returnconst left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX - 300const 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?.nodeValueconst anchorOffset = select.anchorOffsetconst focusOffset = select.focusOffsetconst nodeValueSatrtIndex = markContent.value?.indexOf(nodeValue)selectedText.content = select.toString()if (anchorOffset < focusOffset) {//从左到右标注selectedText.start = nodeValueSatrtIndex + anchorOffsetselectedText.end = nodeValueSatrtIndex + focusOffset} else {//从右到左selectedText.start = nodeValueSatrtIndex + focusOffsetselectedText.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_colorelement.style.userSelect = 'none'element.style.paddingBottom = '6px'element.onclick = function(e) {console.log('cccccc', m)const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX - 300editTag.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 : 500tagInfo.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 实现文字标注功能

效果图 安装依赖 npm install mark.js --save-dev npm i nanoid代码块 <template><!-- 文档标注 --><header><el-buttontype"primary":disabled"selectedTextList.length 0 ? true : false"ghostclick"handleAllDelete"…...

运筹优化 | 模拟退火求解旅行商问题 | Python实现

"""模拟退火旅行商""" import random import numpy as np import math import time import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False location np.loadtxt(city_location.t…...

1017 A除以B

本题要求计算 A/B&#xff0c;其中 A 是不超过 1000 位的正整数&#xff0c;B 是 1 位正整数。你需要输出商数 Q 和余数 R&#xff0c;使得 ABQR 成立。 输入格式&#xff1a; 输入在一行中依次给出 A 和 B&#xff0c;中间以 1 空格分隔。 输出格式&#xff1a; 在一行中依…...

SAP UI5 walkthrough step8 Translatable Texts

在这个章节&#xff0c;我们会将一些文本常量独立出一个资源文件 这样的话&#xff0c;可以方便这些文本常量被翻译成任意的语言 这种国际化的操作&#xff0c;我们一般命名为i18n 新建一个文件i18n.properties webapp/i18n/i18n.properties (New) showHelloButtonTextSay …...

RocketMQ-源码架构二

梳理一些比较完整&#xff0c;比较复杂的业务线 消息持久化设计 RocketMQ的持久化文件结构 消息持久化也就是将内存中的消息写入到本地磁盘的过程。而磁盘IO操作通常是一个很耗性能&#xff0c;很慢的操作&#xff0c;所以&#xff0c;对消息持久化机制的设计&#xff0c;是…...

Unity_ET框架项目-斗地主_启动运行流程

unity_ET框架项目-斗地主_启动运行流程 项目源码地址&#xff1a; Viagi/LandlordsCore: ET斗地主Demohttps://github.com/Viagi/LandlordsCore下载项目到本地。 启动运行步骤&#xff1a; 下载目录如下&#xff1a; 1. VS&#xff08;我用是2022版VisualStudio&#xff09…...

自动化测试框架 —— pytest框架入门篇

今天就给大家说一说pytest框架。 今天这篇文章呢&#xff0c;会从以下几个方面来介绍&#xff1a; 01、pytest框架介绍 pytest 是 python 的第三方单元测试框架&#xff0c;比自带 unittest 更简洁和高效&#xff0c;支持非常丰富的插件&#xff0c;同时兼容 unittest 框架。…...

String类详解

String类详解 大家好&#xff0c;我是免费搭建查券返利机器人赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 解密String类&#xff1a;探秘Java中的字符串魔法 在Java的世界里&#xff0c;String类犹如一位魔法…...

Linux高级管理--安装MySQL数据库系统

MySQL服务基础 MySQL.是一个真正的多线程、多用户的SQL数据库服务&#xff0c;凭借其高性能、高可靠和易于使 用的特性&#xff0c;成为服务器领域中最受欢迎的开源数据库系统。在2008年以前&#xff0c;MySOL项目由MySQL AB公司进行开发&#xff0c;发布和支持&#xff0c;之后…...

团建策划信息展示服务预约小程序效果如何

团建是中大型企业商家每年举办的员工活动&#xff0c;其形式多样化、具备全部参与的娱乐性。但在实际策划流程及内容时&#xff0c;部分公司便会难以入手&#xff0c;术业有专攻&#xff0c;这个时候团建策划公司便会发挥效果。 如拓展训练、露营、运动会、体育竞技等往往更具…...

一个Redis实例最多能存放多少keys

程序员的公众号&#xff1a;源1024&#xff0c;获取更多资料&#xff0c;无加密无套路&#xff01; 最近整理了一份大厂面试资料《史上最全大厂面试题》&#xff0c;Springboot、微服务、算法、数据结构、Zookeeper、Mybatis、Dubbo、linux、Kafka、Elasticsearch、数据库等等 …...

K8S(四)—pod详解

目录 pod介绍Pod的概念&#xff1a;Pod的特性&#xff1a;Pod的配置&#xff1a;Pod的控制&#xff1a;示例 YAML 文件&#xff1a; pod启动流程问题 两种方式启动镜像的升级和回滚更新 Deployment&#xff1a;回滚检查 Deployment 历史版本回滚到之前的修订版本缩放 Deploymen…...

shiro Filter加载和执行 源码解析

一、背景 在使用若依框架&#xff08;前后端不分离包含shiro安全框架&#xff09;时&#xff0c;发现作者添加了验证码、登录帐号控制等自定义过滤器&#xff0c;于是对自定的过滤器加载和执行流程产生疑问。下面以验证码过滤器为例&#xff0c;对源码解析。注意类之间的继承关…...

IDEA上传jar包到Maven

mvn install:install-file //固定格式&#xff0c;maven的语法 -Dfilealibaba-sdk-1.0.0.jar //这里填写包的路径&#xff0c;因为我们是在当前目录所以只需要输入包名即可 -DgroupIdcom.qiehua.csdn //这里填写包的groupId&#xff0c;之后作为pom.xml中引用的gr…...

JavaScript——基本语法

1.定义变量&#xff1a; 变量类型 变量名 变量值 var关键字声明变量 es6版本以上 var 可写可不写 <script>// 定义变量&#xff1a;变量类型 变量名 变量值 var关键字声明变量 es6版本以上 var 可写可不写var num 2;</script>2.条件控制 <script>var …...

一款最近很火的开源低代码平台

低代码平台近年来获得大量融资的原因是多方面的。首先&#xff0c;低代码平台代表了软件开发领域的一个重要趋势&#xff0c;即通过简化编程过程来降低技术门槛&#xff0c;使非专业开发者也能构建应用程序。这为那些希望加速数字化转型的企业提供了新的可能性&#xff0c;因此…...

vue之代理配置devServer(vue.config.js)片段

关于vue.config.js的部分配置解析&#xff1a;首先看下面一段配置 devServer: { open: process.platform darwin,//true or false &#xff08;true则启动项目自动打开系统自带浏览器&#xff09; host: 0.0.0.0, // 配置devServer服务监听的地址 比如&#xff1a;想让局域网…...

CTD测试流程

连接 连接17Plus&#xff0c;用usb转232线&#xff0c;db9公针2、3分别接Data I/O的2、3。DB9的5接Data I/O的1。尼龙塞子打开状态。不用闭合。 软件连接 打开SeaTermAF V2&#xff0c;注意打开前先把串口插上&#xff0c;否则软件读不到串口。如果读不到&#xff0c;就在插…...

面试经典150题(15-19)

leetcode 150道题 计划花两个月时候刷完&#xff0c;今天&#xff08;第七天&#xff09;完成了5道(15-19)150&#xff1a; 今天这些都是我之前做过的&#xff0c;还有就是今天的全都是模拟过程。。所以做的还算快。 15&#xff08;13. 罗马数字转整数&#xff09; 题目描述&a…...

Linux下的网络服务

一般来说&#xff0c;各种操作系统在网络方面的性能比较是这样的顺序BSD>Linux>Win NT>Win 9X, 由此说来&#xff0c;Linux的网络功能仅次于UNIX&#xff0c;而强于Win NT和其它的视窗系列产品&#xff0c;对于Win2000我还不能评价太多&#xff0c;因为不是很熟。 Lin…...

基于Flask实现的医疗保险欺诈识别监测模型

基于Flask实现的医疗保险欺诈识别监测模型 项目截图 项目简介 社会医疗保险是国家通过立法形式强制实施&#xff0c;由雇主和个人按一定比例缴纳保险费&#xff0c;建立社会医疗保险基金&#xff0c;支付雇员医疗费用的一种医疗保险制度&#xff0c; 它是促进社会文明和进步的…...

【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)

可以使用Sqliteviz这个网站免费编写sql语句&#xff0c;它能够让用户直接在浏览器内练习SQL的语法&#xff0c;不需要安装任何软件。 链接如下&#xff1a; sqliteviz 注意&#xff1a; 在转写SQL语法时&#xff0c;关键字之间有一个特定的顺序&#xff0c;这个顺序会影响到…...

使用van-uploader 的UI组件,结合vue2如何实现图片上传组件的封装

以下是基于 vant-ui&#xff08;适配 Vue2 版本 &#xff09;实现截图中照片上传预览、删除功能&#xff0c;并封装成可复用组件的完整代码&#xff0c;包含样式和逻辑实现&#xff0c;可直接在 Vue2 项目中使用&#xff1a; 1. 封装的图片上传组件 ImageUploader.vue <te…...

【从零学习JVM|第三篇】类的生命周期(高频面试题)

前言&#xff1a; 在Java编程中&#xff0c;类的生命周期是指类从被加载到内存中开始&#xff0c;到被卸载出内存为止的整个过程。了解类的生命周期对于理解Java程序的运行机制以及性能优化非常重要。本文会深入探寻类的生命周期&#xff0c;让读者对此有深刻印象。 目录 ​…...

智能职业发展系统:AI驱动的职业规划平台技术解析

智能职业发展系统&#xff1a;AI驱动的职业规划平台技术解析 引言&#xff1a;数字时代的职业革命 在当今瞬息万变的就业市场中&#xff0c;传统的职业规划方法已无法满足个人和企业的需求。据统计&#xff0c;全球每年有超过2亿人面临职业转型困境&#xff0c;而企业也因此遭…...

初级程序员入门指南

初级程序员入门指南 在数字化浪潮中&#xff0c;编程已然成为极具价值的技能。对于渴望踏入程序员行列的新手而言&#xff0c;明晰入门路径与必备知识是开启征程的关键。本文将为初级程序员提供全面的入门指引。 一、明确学习方向 &#xff08;一&#xff09;编程语言抉择 编…...

十、【ESP32开发全栈指南: TCP客户端】

一、TCP协议核心特性回顾 TCP与UDP关键差异 特性TCPUDP连接方式面向连接 (三次握手)无连接可靠性可靠传输 (重传/排序/校验)尽力交付数据顺序保证数据按序到达不保证顺序流控制滑动窗口机制无流控制传输效率协议开销大头部开销小适用场景文件传输、网页浏览实时音视频、广播通…...

自建 dnslog 回显平台:渗透测试场景下的隐蔽回显利器

&#x1f50d; 背景介绍 在渗透测试与红队评估过程中&#xff0c;DNS 外带&#xff08;DNS Exfiltration&#xff09; 是一种常见且隐蔽的通信通道。由于多数目标环境默认具备外网 DNS 解析能力&#xff0c;即便在 无回显、无文件上传权限 的条件下&#xff0c;仍可通过 DNS 请…...

DJango知识-模型类

一.项目创建 在想要将项目创键的目录下,输入cmd (进入命令提示符)在cmd中输入:Django-admin startproject 项目名称 (创建项目)cd 项目名称 (进入项目)Django-admin startapp 程序名称 (创建程序)python manage.py runserver 8080 (运行程序)将弹出的网址复制到浏览器中…...

QuaggaJS用法详解

QuaggaJS简介 QuaggaJS是一个强大的JavaScript库&#xff0c;专门用于在浏览器环境中进行条形码和二维码识别。它支持多种条形码格式&#xff0c;包括Code 128、Code 39、EAN、QR码等&#xff0c;并且可以直接调用设备摄像头进行实时扫描。 QuaggaJS核心功能与用法 1. 基本配…...