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,其中 A 是不超过 1000 位的正整数,B 是 1 位正整数。你需要输出商数 Q 和余数 R,使得 ABQR 成立。 输入格式: 输入在一行中依次给出 A 和 B,中间以 1 空格分隔。 输出格式: 在一行中依…...
SAP UI5 walkthrough step8 Translatable Texts
在这个章节,我们会将一些文本常量独立出一个资源文件 这样的话,可以方便这些文本常量被翻译成任意的语言 这种国际化的操作,我们一般命名为i18n 新建一个文件i18n.properties webapp/i18n/i18n.properties (New) showHelloButtonTextSay …...
RocketMQ-源码架构二
梳理一些比较完整,比较复杂的业务线 消息持久化设计 RocketMQ的持久化文件结构 消息持久化也就是将内存中的消息写入到本地磁盘的过程。而磁盘IO操作通常是一个很耗性能,很慢的操作,所以,对消息持久化机制的设计,是…...
Unity_ET框架项目-斗地主_启动运行流程
unity_ET框架项目-斗地主_启动运行流程 项目源码地址: Viagi/LandlordsCore: ET斗地主Demohttps://github.com/Viagi/LandlordsCore下载项目到本地。 启动运行步骤: 下载目录如下: 1. VS(我用是2022版VisualStudio)…...
自动化测试框架 —— pytest框架入门篇
今天就给大家说一说pytest框架。 今天这篇文章呢,会从以下几个方面来介绍: 01、pytest框架介绍 pytest 是 python 的第三方单元测试框架,比自带 unittest 更简洁和高效,支持非常丰富的插件,同时兼容 unittest 框架。…...
String类详解
String类详解 大家好,我是免费搭建查券返利机器人赚佣金就用微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿! 解密String类:探秘Java中的字符串魔法 在Java的世界里,String类犹如一位魔法…...
Linux高级管理--安装MySQL数据库系统
MySQL服务基础 MySQL.是一个真正的多线程、多用户的SQL数据库服务,凭借其高性能、高可靠和易于使 用的特性,成为服务器领域中最受欢迎的开源数据库系统。在2008年以前,MySOL项目由MySQL AB公司进行开发,发布和支持,之后…...
团建策划信息展示服务预约小程序效果如何
团建是中大型企业商家每年举办的员工活动,其形式多样化、具备全部参与的娱乐性。但在实际策划流程及内容时,部分公司便会难以入手,术业有专攻,这个时候团建策划公司便会发挥效果。 如拓展训练、露营、运动会、体育竞技等往往更具…...
一个Redis实例最多能存放多少keys
程序员的公众号:源1024,获取更多资料,无加密无套路! 最近整理了一份大厂面试资料《史上最全大厂面试题》,Springboot、微服务、算法、数据结构、Zookeeper、Mybatis、Dubbo、linux、Kafka、Elasticsearch、数据库等等 …...
K8S(四)—pod详解
目录 pod介绍Pod的概念:Pod的特性:Pod的配置:Pod的控制:示例 YAML 文件: pod启动流程问题 两种方式启动镜像的升级和回滚更新 Deployment:回滚检查 Deployment 历史版本回滚到之前的修订版本缩放 Deploymen…...
shiro Filter加载和执行 源码解析
一、背景 在使用若依框架(前后端不分离包含shiro安全框架)时,发现作者添加了验证码、登录帐号控制等自定义过滤器,于是对自定的过滤器加载和执行流程产生疑问。下面以验证码过滤器为例,对源码解析。注意类之间的继承关…...
IDEA上传jar包到Maven
mvn install:install-file //固定格式,maven的语法 -Dfilealibaba-sdk-1.0.0.jar //这里填写包的路径,因为我们是在当前目录所以只需要输入包名即可 -DgroupIdcom.qiehua.csdn //这里填写包的groupId,之后作为pom.xml中引用的gr…...
JavaScript——基本语法
1.定义变量: 变量类型 变量名 变量值 var关键字声明变量 es6版本以上 var 可写可不写 <script>// 定义变量:变量类型 变量名 变量值 var关键字声明变量 es6版本以上 var 可写可不写var num 2;</script>2.条件控制 <script>var …...
一款最近很火的开源低代码平台
低代码平台近年来获得大量融资的原因是多方面的。首先,低代码平台代表了软件开发领域的一个重要趋势,即通过简化编程过程来降低技术门槛,使非专业开发者也能构建应用程序。这为那些希望加速数字化转型的企业提供了新的可能性,因此…...
vue之代理配置devServer(vue.config.js)片段
关于vue.config.js的部分配置解析:首先看下面一段配置 devServer: { open: process.platform darwin,//true or false (true则启动项目自动打开系统自带浏览器) host: 0.0.0.0, // 配置devServer服务监听的地址 比如:想让局域网…...
CTD测试流程
连接 连接17Plus,用usb转232线,db9公针2、3分别接Data I/O的2、3。DB9的5接Data I/O的1。尼龙塞子打开状态。不用闭合。 软件连接 打开SeaTermAF V2,注意打开前先把串口插上,否则软件读不到串口。如果读不到,就在插…...
面试经典150题(15-19)
leetcode 150道题 计划花两个月时候刷完,今天(第七天)完成了5道(15-19)150: 今天这些都是我之前做过的,还有就是今天的全都是模拟过程。。所以做的还算快。 15(13. 罗马数字转整数) 题目描述&a…...
Linux下的网络服务
一般来说,各种操作系统在网络方面的性能比较是这样的顺序BSD>Linux>Win NT>Win 9X, 由此说来,Linux的网络功能仅次于UNIX,而强于Win NT和其它的视窗系列产品,对于Win2000我还不能评价太多,因为不是很熟。 Lin…...
大数据零基础学习day1之环境准备和大数据初步理解
学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 (1)设置网关 打开VMware虚拟机,点击编辑…...
令牌桶 滑动窗口->限流 分布式信号量->限并发的原理 lua脚本分析介绍
文章目录 前言限流限制并发的实际理解限流令牌桶代码实现结果分析令牌桶lua的模拟实现原理总结: 滑动窗口代码实现结果分析lua脚本原理解析 限并发分布式信号量代码实现结果分析lua脚本实现原理 双注解去实现限流 并发结果分析: 实际业务去理解体会统一注…...
稳定币的深度剖析与展望
一、引言 在当今数字化浪潮席卷全球的时代,加密货币作为一种新兴的金融现象,正以前所未有的速度改变着我们对传统货币和金融体系的认知。然而,加密货币市场的高度波动性却成为了其广泛应用和普及的一大障碍。在这样的背景下,稳定…...
SAP学习笔记 - 开发26 - 前端Fiori开发 OData V2 和 V4 的差异 (Deepseek整理)
上一章用到了V2 的概念,其实 Fiori当中还有 V4,咱们这一章来总结一下 V2 和 V4。 SAP学习笔记 - 开发25 - 前端Fiori开发 Remote OData Service(使用远端Odata服务),代理中间件(ui5-middleware-simpleproxy)-CSDN博客…...
Xen Server服务器释放磁盘空间
disk.sh #!/bin/bashcd /run/sr-mount/e54f0646-ae11-0457-b64f-eba4673b824c # 全部虚拟机物理磁盘文件存储 a$(ls -l | awk {print $NF} | cut -d. -f1) # 使用中的虚拟机物理磁盘文件 b$(xe vm-disk-list --multiple | grep uuid | awk {print $NF})printf "%s\n"…...
在QWebEngineView上实现鼠标、触摸等事件捕获的解决方案
这个问题我看其他博主也写了,要么要会员、要么写的乱七八糟。这里我整理一下,把问题说清楚并且给出代码,拿去用就行,照着葫芦画瓢。 问题 在继承QWebEngineView后,重写mousePressEvent或event函数无法捕获鼠标按下事…...
Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...
LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf
FTP 客服管理系统 实现kefu123登录,不允许匿名访问,kefu只能访问/data/kefu目录,不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...
给网站添加live2d看板娘
给网站添加live2d看板娘 参考文献: stevenjoezhang/live2d-widget: 把萌萌哒的看板娘抱回家 (ノ≧∇≦)ノ | Live2D widget for web platformEikanya/Live2d-model: Live2d model collectionzenghongtu/live2d-model-assets 前言 网站环境如下,文章也主…...
go 里面的指针
指针 在 Go 中,指针(pointer)是一个变量的内存地址,就像 C 语言那样: a : 10 p : &a // p 是一个指向 a 的指针 fmt.Println(*p) // 输出 10,通过指针解引用• &a 表示获取变量 a 的地址 p 表示…...
