当前位置: 首页 > 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…...

Fan Control:Windows平台终极风扇控制解决方案

Fan Control&#xff1a;Windows平台终极风扇控制解决方案 【免费下载链接】FanControl.Releases This is the release repository for Fan Control, a highly customizable fan controlling software for Windows. 项目地址: https://gitcode.com/GitHub_Trending/fa/FanCon…...

中国500万医生的新AI:顶刊独家联手,卷的就是证据源

金磊 发自 杭州量子位 | 公众号 QbitAI很反差。明明是一场AI的发布会&#xff0c;台下却坐满了医学界的大佬们&#xff1a;有北大、清华的&#xff0c;有浙江、上海的&#xff0c;甚至医学顶刊BMJ集团的主编都来围观了……△图片由AI生成为啥会这样&#xff1f;因为阿里健康正式…...

从热设计小白到专家:我是如何用RC6-4-01这颗TEC搞定激光器温控的(真实项目复盘)

从热设计小白到专家&#xff1a;我是如何用RC6-4-01这颗TEC搞定激光器温控的&#xff08;真实项目复盘&#xff09; 激光器温控从来不是简单的"制冷片贴上去就行"。去年接手某光纤激光器项目时&#xff0c;面对客户要求的0.1℃控温精度&#xff0c;我盯着规格书里密密…...

为ClaudeCode配置Taotoken作为稳定可靠的API供应商

&#x1f680; 告别海外账号与网络限制&#xff01;稳定直连全球优质大模型&#xff0c;限时半价接入中。 &#x1f449; 点击领取海量免费额度 为ClaudeCode配置Taotoken作为稳定可靠的API供应商 Claude Code 是一款广受开发者欢迎的编程助手工具&#xff0c;它依赖于后端的大…...

避坑指南:Halcon在C# WinForm中图像处理的内存管理与窗口显示问题

Halcon与C#联合开发中的内存管理与窗口显示避坑指南 引言 在工业视觉应用开发中&#xff0c;Halcon与C#的联合开发模式因其高效性和灵活性而广受欢迎。然而&#xff0c;许多开发者在实际项目中常会遇到一些棘手的"坑"&#xff0c;尤其是内存管理和窗口显示方面的问题…...

酒吧数字化方案:Java德州扑克小酒馆扫码点餐预约系统源码

在消费升级与数字化转型的大背景下&#xff0c;中小型德州扑克小酒馆的运营模式正逐步从“人工主导”向“数字化赋能”转变。不同于传统酒吧&#xff0c;德州扑克小酒馆以“休闲娱乐餐饮服务”为核心&#xff0c;其运营痛点集中在点餐效率低、预约管理乱、桌台调度难、合规管控…...

网络安全5大高薪赛道,哪条是你的职业快车道?

1. 政企安全&#xff1a;国家队的黄金赛道 政企安全领域就像网络安全行业的"公务员体系"&#xff0c;稳定性和薪资待遇都处于行业头部水平。我接触过不少从互联网公司转行做政企安全的工程师&#xff0c;他们普遍反馈"虽然加班也不少&#xff0c;但项目预算充足…...

TVA动态阈值在昇腾310的适配要点

重磅预告&#xff1a;本专栏将独家连载系列丛书《智能体视觉技术与应用》部分精华内容&#xff0c;该书是世界首套系统阐述“因式智能体”视觉理论与实践的专著&#xff0c;特邀美国 TypeOne 公司首席科学家、斯坦福大学博士 Bohan 担任技术顾问。Bohan先生师从美国三院院士、“…...

ESUM模型:统一处理多拜耳模式的去马赛克技术

1. 去马赛克技术演进与多拜耳模式挑战去马赛克&#xff08;Demosaicing&#xff09;是数字图像处理中一项基础而关键的技术&#xff0c;它负责将传感器捕获的原始马赛克数据转换为全彩色图像。传统单拜耳&#xff08;Single-Bayer&#xff09;模式采用RGGB排列&#xff0c;每个…...

LeetCode 01矩阵中距离题解

LeetCode 01矩阵中距离题解 题目描述 给定一个 01 矩阵&#xff0c;找到每个 0 到最近的 0 的距离。 示例&#xff1a; 输入&#xff1a;mat [[0,0,0],[0,1,0],[1,1,1]]输出&#xff1a;[[0,0,0],[0,1,0],[1,2,1]] 解题思路 方法&#xff1a;BFS 思路&#xff1a; 使用 BFS 从…...