vue3 + mark.js 实现文字标注功能
2023-12-13 03:05:11
效果图
安装依赖
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>
文章来源:https://blog.csdn.net/qq_33591873/article/details/134857918
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!