效果图
安装依赖
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' 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 } = ttagInfo. 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_colorelement. 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? . 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 = `2 px 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 - 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>