效果图
安装依赖
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'
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: `#6 ADE05FF `,
tag_id: 'tag_id2',
} ,
{
tag_name: '3级' ,
tag_color: `#DE058BFF `,
tag_id: 'tag_id3',
} ,
{
tag_name: '4级' ,
tag_color: `#9205D EFF `,
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
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 = `2 px 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 = `2 px 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;
align- items: center;
padding: 0 24 px;
height: 80 px;
border- bottom: 1 px solid #e5e7eb;
user- select: none;
background: #fff;
}
main {
background: #fff;
margin: 24 px;
height: 80 vh;
padding: 24 px;
overflow- y: auto;
position: relative;
box- shadow: 0 3 px 8 px 0 rgb ( 0 0 0 / 13 % ) ;
. text {
color: #333 ;
font- weight: 500 ;
font- size: 16 px;
line- height: 50 px;
}
. tag- box {
position: absolute;
z- index: 10 ;
width: 150 px;
max- height: 40 vh;
overflow- y: auto;
background: #fff;
border- radius: 4 px;
box- shadow: 0 9 px 28 px 8 px rgb ( 0 0 0 / 3 % ) , 0 6 px 16 px 4 px rgb ( 0 0 0 / 9 % ) ,
0 3 px 6 px - 2 px rgb ( 0 0 0 / 20 % ) ;
user- select: none;
. tag- name {
background: rgba ( 243 , 244 , 246 , var ( -- tw- bg- opacity) ) ;
font- size: 14 px;
cursor: pointer;
display: flex;
justify- content: space- between;
align- items: center;
padding: 4 px 8 px;
margin- top: 8 px;
}
. tag- name: nth- of- type ( 1 ) {
margin- top: 0 ;
}
}
. edit- tag {
position: absolute;
z- index: 20 ;
padding: 16 px;
cursor: pointer;
width: 40 px;
background: #fff;
border- radius: 4 px;
box- shadow: 0 9 px 28 px 8 px rgb ( 0 0 0 / 3 % ) , 0 6 px 16 px 4 px rgb ( 0 0 0 / 9 % ) ,
0 3 px 6 px - 2 px rgb ( 0 0 0 / 20 % ) ;
user- select: none;
}
:: selection {
background: rgb ( 51 51 51 / 20 % ) ;
}
}
< / style>