You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
635 lines
20 KiB
635 lines
20 KiB
<template>
|
|
<div>
|
|
<v-contextmenu ref="contextmenu">
|
|
<v-contextmenu-item v-if="!disabled">
|
|
<el-popover
|
|
v-model="visible"
|
|
trigger="click"
|
|
@hide="resetNote"
|
|
>
|
|
<el-form ref="form" :model="note_form" :rules="rules" label-width="80px">
|
|
<el-form-item :label="$t('note.table.original_text')">
|
|
<el-input autosize type="textarea" v-model="note_form.original_text" disabled></el-input>
|
|
</el-form-item>
|
|
<el-form-item :label="$t('read.form.note_title')" prop="note_title">
|
|
<el-input v-model="note_form.note_title"
|
|
:placeholder="$t('input_please', { keyword: this.$t('read.form.note_title') })"></el-input>
|
|
</el-form-item>
|
|
<el-form-item :label="$t('read.form.note_content')" prop="note_content">
|
|
<el-input autosize type="textarea" v-model="note_form.note_content"
|
|
:placeholder="$t('input_please', { keyword: this.$t('read.form.note_content') })"></el-input>
|
|
</el-form-item>
|
|
<el-form-item>
|
|
<el-button @click="addNote">{{$t('button.add')}}</el-button>
|
|
<el-button @click="visible=false">{{$t('button.cancel')}}</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
<el-button slot="reference">{{$t('read.contextmenu.add')}}</el-button>
|
|
</el-popover>
|
|
</v-contextmenu-item>
|
|
<v-contextmenu-item>
|
|
<el-button @click="updateFind(true);queryContent()" class="w-100">{{$t('read.contextmenu.search')}}</el-button>
|
|
</v-contextmenu-item>
|
|
</v-contextmenu>
|
|
<el-tabs v-model="activeName" type="card" closable @tab-remove="removeTab">
|
|
<el-tab-pane :label="key" :name="key" v-for="(item,key) in openList" :key="key">
|
|
<el-row>
|
|
<el-col :span="18">
|
|
<el-form :inline="true" v-if="showSearch">
|
|
<el-form-item :label="$t('read.form.keyword')">
|
|
<el-input @input="queryContent" :placeholder="$t('input_please',{keyword:$t('read.form.keyword')})"
|
|
v-model="search_form.keyword"
|
|
maxlength="10"
|
|
show-word-limit>
|
|
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
|
</el-input>
|
|
</el-form-item>
|
|
<template v-if="num>0">
|
|
<el-form-item>{{search_index+1+'/'+num}}</el-form-item>
|
|
<el-tooltip :content="$t('read.tip.search_prev')">
|
|
<el-image src="/prev.svg" class="img"
|
|
@click="search_index>0?search_index--:search_index=num-1;change_search_index()"></el-image>
|
|
</el-tooltip>
|
|
<el-tooltip :content="$t('read.tip.search_next')">
|
|
<el-image src="/next.svg" class="img"
|
|
@click="search_index<num-1?search_index++:search_index=0;change_search_index()"></el-image>
|
|
</el-tooltip>
|
|
</template>
|
|
</el-form>
|
|
<pre class="content" v-contextmenu:contextmenu :ref="'edit'+item.title" @mouseup="show"
|
|
@contextmenu="selectText" @scroll="scrollContent" v-html="item.content"
|
|
/>
|
|
</el-col>
|
|
<el-col :span="5" class="ml3 note-list">
|
|
<template v-if="noteList.length>0">
|
|
<el-row v-for="(item,index) in noteList" :key="index" class="mb2">
|
|
<el-tooltip :content="$t('read.tip.click_note_list')" :disabled="item.isEdit">
|
|
<el-card>
|
|
<div slot="header" class="clearfix">
|
|
<el-row type="flex" justify="space-around">
|
|
<span>{{item.title}}</span>
|
|
<el-button-group>
|
|
<el-button size="mini" type="primary" @click="edit(item)" icon="el-icon-edit"></el-button>
|
|
<el-popconfirm
|
|
@onConfirm="del(item,index)"
|
|
:title="$t('read.tip.del_confirm')"
|
|
>
|
|
<el-button size="mini" type="danger" slot="reference" icon="el-icon-delete"></el-button>
|
|
</el-popconfirm>
|
|
</el-button-group>
|
|
</el-row>
|
|
</div>
|
|
|
|
<div class="text item" @click="jump(item)" @mouseover="highlighting(item)"
|
|
@mouseout="reset(item)">
|
|
<transition v-if="item.isEdit" @after-leave="save(item)">
|
|
<el-tooltip class="item" effect="dark" :content="$t('read.tip.auto_save')"
|
|
v-model="item.isEdit">
|
|
<el-input autosize type="textarea" v-model="item.content"></el-input>
|
|
</el-tooltip>
|
|
</transition>
|
|
<template v-else>
|
|
<pre style="background-color: white">{{item.content}}</pre>
|
|
</template>
|
|
</div>
|
|
</el-card>
|
|
</el-tooltip>
|
|
<el-alert v-if="item.message"
|
|
@close="item.message=null;item.type=null"
|
|
:title="item.message"
|
|
:type="item.type">
|
|
</el-alert>
|
|
</el-row>
|
|
</template>
|
|
<h1 v-else>{{$t('read.tip.note_zero')}}</h1>
|
|
</el-col>
|
|
</el-row>
|
|
</el-tab-pane>
|
|
</el-tabs>
|
|
<el-col :span="21">
|
|
<el-row class="mt2">
|
|
<el-col :span="7" v-for="(item,index) in stars" :key="index">
|
|
<h3 class="center">{{$t('read.tip.rating_'+(index+1))}}</h3>
|
|
<el-rate
|
|
class="center star-icon"
|
|
v-model="item.value"
|
|
show-score
|
|
score-template="{value}分"
|
|
:disabled="item.value>0">
|
|
</el-rate>
|
|
</el-col>
|
|
</el-row>
|
|
<el-row class="mt2" type="flex" justify="center">
|
|
<el-col :span="3">
|
|
<el-button class="star-button" v-if="show_star_button" @click="submitRating">{{$t('button.rating')}}
|
|
</el-button>
|
|
</el-col>
|
|
</el-row>
|
|
</el-col>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
|
|
import Vue from 'vue'
|
|
|
|
export default Vue.extend({
|
|
name: 'read',
|
|
layout: 'my-pre',
|
|
data() {
|
|
return {
|
|
//显示搜索框
|
|
showSearch: false,
|
|
//显示笔记表单
|
|
visible: false,
|
|
//正文搜索
|
|
search_form: {
|
|
keyword: ''
|
|
},
|
|
//搜索结果记录数
|
|
num: 0,
|
|
//搜索结果索引
|
|
search_index: -1,
|
|
//笔记表单
|
|
note_form: {
|
|
note_id: '',
|
|
note_title: '',
|
|
note_content: '',
|
|
original_text: ''
|
|
},
|
|
//表单校验规则
|
|
rules: {
|
|
note_title: [{
|
|
required: true,
|
|
message: this.$t('input_please', { keyword: this.$t('read.form.note_title') }),
|
|
trigger: 'blur'
|
|
}],
|
|
note_content: [{
|
|
required: true,
|
|
message: this.$t('input_please', { keyword: this.$t('read.form.note_content') }),
|
|
trigger: 'blur'
|
|
}]
|
|
},
|
|
//活动的论文标签
|
|
activeName: '',
|
|
//笔记列表
|
|
noteList: [],
|
|
//禁用右键菜单标志位
|
|
disabled: true,
|
|
//替换目标
|
|
replace: {},
|
|
stars: [],
|
|
show_star_button: false
|
|
}
|
|
},
|
|
computed: {
|
|
//打开的论文标签
|
|
openList() {
|
|
return this.$store.state.read.read
|
|
},
|
|
//当前激活的正文元素
|
|
pre() {
|
|
return this.$refs['edit' + this.activeName][0]
|
|
},
|
|
//当前激活的论文实体
|
|
activeContent() {
|
|
return this.$store.state.read.read[this.activeName]
|
|
}
|
|
},
|
|
watch: {
|
|
//激活的论文标签
|
|
activeName(newVal) {
|
|
this.$store.commit('read/choose', newVal)
|
|
this.doScroll()
|
|
if (this.activeContent.find && this.activeContent.find.showSearch) {
|
|
this.showSearch = this.activeContent.find.showSearch
|
|
this.search_form.keyword = this.activeContent.find.keyword
|
|
let search_index = this.activeContent.find.search_index
|
|
this.queryContent()
|
|
this.search_index = search_index
|
|
this.change_search_index()
|
|
} else {
|
|
this.showSearch = false
|
|
this.search_form = {
|
|
keyword: ''
|
|
}
|
|
this.num = 0
|
|
this.search_index = -1
|
|
}
|
|
|
|
},
|
|
showSearch(newVal) {
|
|
if (!newVal) {
|
|
let pre: HTMLElement = this.pre
|
|
this.search_form.keyword = ''
|
|
this.resetQuery(pre)
|
|
}
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
//查找评价记录
|
|
queryRating(){
|
|
let that=this
|
|
this.GLOBAL.fetchGet('/v1/api/paper/findRating', { paperId:this.activeContent.id },function(res) {
|
|
if(res.code==='200'){
|
|
if(res.data===null){
|
|
that.stars=[{ value: 0 }, { value: 0 }, { value: 0 }]
|
|
}else{
|
|
that.stars=[{ value: res.data.score1 }, { value: res.data.score2 }, { value: res.data.score3 }]
|
|
}
|
|
that.show_star_button=res.data===null
|
|
}else{
|
|
that.$message.error(that.$t('error_500').toString())
|
|
}
|
|
})
|
|
},
|
|
//滚动正文
|
|
scrollContent(e) {
|
|
this.$store.commit('read/scroll', e.target.scrollTop)
|
|
},
|
|
doScroll() {
|
|
this.$nextTick(() => {
|
|
if (this.activeContent.scrollTop) {
|
|
this.pre.scrollTo({
|
|
top: this.activeContent.scrollTop
|
|
})
|
|
}
|
|
})
|
|
},
|
|
//激活选中的搜索结果
|
|
change_search_index() {
|
|
let ele = this.pre.querySelectorAll('span[class^=search-light]')[this.search_index]
|
|
ele.classList.add('choose-search-text')
|
|
ele.scrollIntoView({
|
|
block: 'center'
|
|
})
|
|
for (let index = 0; index < this.num; index++) {
|
|
if (index !== this.search_index) {
|
|
this.pre.querySelectorAll('span[class^=search-light]')[index].classList.remove('choose-search-text')
|
|
}
|
|
}
|
|
|
|
this.updateFind(this.showSearch)
|
|
},
|
|
updateFind(showSearch = false) {
|
|
this.showSearch = showSearch
|
|
this.$store.commit('read/find', {
|
|
showSearch: showSearch,
|
|
keyword: this.search_form.keyword,
|
|
search_index: this.search_index
|
|
})
|
|
},
|
|
//重置搜索
|
|
resetQuery(pre: HTMLElement) {
|
|
for (let span of pre.querySelectorAll('span[class^=search-light]')) {
|
|
let text = document.createTextNode(span.innerText)
|
|
span.replaceWith(text)
|
|
}
|
|
pre.normalize()
|
|
this.num = 0
|
|
this.search_index = -1
|
|
},
|
|
//正文搜索
|
|
queryContent() {
|
|
let pre: HTMLElement = this.pre
|
|
this.resetQuery(pre)
|
|
if (this.search_form.keyword.length === 0) {
|
|
let selection = getSelection()
|
|
if (selection && selection.toString().length > 0) {
|
|
this.search_form.keyword = selection.toString()
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
let replaceObj = []
|
|
for (let node of pre.childNodes) {
|
|
let content = node.nodeType === 3 ? node.data : node.innerText
|
|
let matchAll = content.matchAll(new RegExp(this.search_form.keyword, 'g'))
|
|
let replaceNodes = []
|
|
if (node.nodeType === 1 && node.innerText.length === this.search_form.keyword.length) {
|
|
node.classList.add('search-light')
|
|
} else {
|
|
let length = 0
|
|
let prev
|
|
for (let match of matchAll) {
|
|
this.num += 1
|
|
let start = document.createTextNode(content.substr(prev ? prev : 0, match.index - length))
|
|
let span = document.createElement('span')
|
|
span.classList.add('search-light')
|
|
span.innerText = this.search_form.keyword
|
|
prev = match.index + this.search_form.keyword.length
|
|
replaceNodes.push(start, span)
|
|
length += start.data.length + this.search_form.keyword.length
|
|
}
|
|
if (length > 0) {
|
|
replaceNodes.push(document.createTextNode(content.substr(prev)))
|
|
replaceObj.push({
|
|
source: node,
|
|
target: replaceNodes
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let item of replaceObj) {
|
|
let node = item.source
|
|
let replaceNodes = item.target
|
|
if (replaceNodes.length > 0) {
|
|
if (node.nodeType === 3) {
|
|
node.replaceWith(replaceNodes[0])
|
|
for (let index = 1; index < replaceNodes.length; index++) {
|
|
replaceNodes[index - 1].after(replaceNodes[index])
|
|
}
|
|
} else {
|
|
node.innerHTML = ''
|
|
for (let child of replaceNodes) {
|
|
node.appendChild(child)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (this.num > 0) {
|
|
this.search_index = 0
|
|
this.change_search_index()
|
|
} else {
|
|
this.$message.warning({
|
|
message: this.$t('read.tip.search_zero', { keyword: this.search_form.keyword }).toString(),
|
|
showClose: true,
|
|
duration: 2000
|
|
})
|
|
}
|
|
},
|
|
//编辑笔记
|
|
edit(item: any) {
|
|
if ('isEdit' in item) {
|
|
item.isEdit = !item.isEdit
|
|
} else {
|
|
this.$set(item, 'isEdit', true)
|
|
}
|
|
},
|
|
//保存笔记
|
|
save(item: any) {
|
|
this.$set(item, 'type', 'success')
|
|
this.$set(item, 'message', '保存成功')
|
|
},
|
|
//删除笔记
|
|
del(item: any, index: number) {
|
|
this.$set(item, 'type', 'success')
|
|
this.$set(item, 'message', '删除成功')
|
|
this.noteList.splice(index, 1)
|
|
},
|
|
//提交评分
|
|
submitRating() {
|
|
console.info(this.stars)
|
|
let that=this
|
|
this.GLOBAL.fetchJSON('/v1/api/paper/rating','POST',{
|
|
paperId:this.activeContent.id,
|
|
score1:this.stars[0].value,
|
|
score2:this.stars[1].value,
|
|
score3:this.stars[2].value,
|
|
},function(res) {
|
|
if(res.code==='200'){
|
|
that.queryRating()
|
|
that.$message.info(that.$t('read.tip.rating_ok').toString())
|
|
}else{
|
|
that.$message.error(that.$t('error_500').toString())
|
|
}
|
|
})
|
|
},
|
|
//高亮笔记
|
|
highlighting(item: any) {
|
|
let ele = document.getElementById(item.id)
|
|
if (ele && this.GLOBAL.visible_in_container(this.$refs['edit' + this.activeName][0], ele)) {
|
|
ele.classList.add('heightlight')
|
|
}
|
|
},
|
|
reset(item: any) {
|
|
let ele = document.getElementById(item.id)
|
|
if (ele) {
|
|
ele.classList.remove('heightlight')
|
|
}
|
|
},
|
|
//跳转到笔记位置
|
|
jump(item: any) {
|
|
let ele = document.getElementById(item.id)
|
|
if (ele) {
|
|
ele.scrollIntoView({
|
|
block: 'center'
|
|
})
|
|
}
|
|
|
|
this.highlighting(item)
|
|
},
|
|
//是否显示右键菜单
|
|
show() {
|
|
let s = getSelection()
|
|
if (s) {
|
|
this.disabled = s.isCollapsed
|
|
} else {
|
|
this.disabled = true
|
|
}
|
|
},
|
|
//关闭标签
|
|
removeTab(name: string) {
|
|
this.$store.commit('read/close', name)
|
|
if (Object.keys(this.openList).length === 0) {
|
|
this.$router.push(this.localePath('/document'))
|
|
this.$store.commit('menus/none')
|
|
}
|
|
this.activeName = this.$store.state.read.activeName
|
|
},
|
|
resetNote() {
|
|
this.replace = {}
|
|
this.note_form.note_id = ''
|
|
this.note_form.note_title = ''
|
|
this.note_form.note_content = ''
|
|
this.note_form.original_text = ''
|
|
},
|
|
//添加笔记
|
|
addNote() {
|
|
let that = this
|
|
this.$refs.form.validate((valid: boolean) => {
|
|
if (valid && Object.keys(that.replace.replaceVal).length > 0) {
|
|
let id = new Date().getTime()
|
|
let c = document.createElement('span')
|
|
c.setAttribute('id', id.toString())
|
|
that.replace.val.replaceWith(that.replace.replaceVal[0])
|
|
for (let index = 1; index < that.replace.replaceVal.length; index++) {
|
|
that.replace.replaceVal[index - 1].after(that.replace.replaceVal[index])
|
|
}
|
|
that.noteList.unshift({
|
|
id: this.note_form.note_id,
|
|
title: this.note_form.note_title,
|
|
content: this.note_form.note_content
|
|
})
|
|
that.$message.info({
|
|
message: this.$t('read.tip.add_tip_ok').toString(),
|
|
showClose: true,
|
|
duration: 1000
|
|
})
|
|
} else {
|
|
that.$message.error({
|
|
message: this.$t('read.tip.add_tip_fail').toString(),
|
|
showClose: true,
|
|
duration: 1000
|
|
})
|
|
return false
|
|
}
|
|
this.visible = false
|
|
})
|
|
|
|
},
|
|
//获取选中文本
|
|
selectText() {
|
|
let selection = getSelection()
|
|
console.info(selection)
|
|
if (selection && !this.disabled) {
|
|
let range = selection.getRangeAt(0)
|
|
console.info(range)
|
|
if (range.startContainer === range.endContainer) {
|
|
let tip = '不跨节点'
|
|
let anchorNode = range.startContainer
|
|
let bold = document.createElement('span')
|
|
bold.classList.add('bold')
|
|
this.note_form.note_id = this.$uuid.v1()
|
|
bold.setAttribute('id', this.note_form.note_id)
|
|
if (range.endOffset - range.startOffset === anchorNode.length) {
|
|
console.info(tip + '全选')
|
|
bold.innerText = anchorNode.wholeText
|
|
this.replace = {
|
|
val: anchorNode,
|
|
replaceVal: [bold]
|
|
}
|
|
} else {
|
|
console.info(tip + '选中一部分')
|
|
let start = document.createTextNode(anchorNode.substringData(0, range.startOffset))
|
|
let end = document.createTextNode(anchorNode.substringData(range.endOffset, anchorNode.length))
|
|
bold.innerText = anchorNode.substringData(range.startOffset, range.endOffset - range.startOffset)
|
|
this.replace = {
|
|
val: anchorNode,
|
|
replaceVal: [start, bold, end]
|
|
}
|
|
}
|
|
this.note_form.original_text = bold.innerText
|
|
} else {
|
|
let tip = '跨节点'
|
|
let startNode = range.startContainer
|
|
let endNode = range.endContainer
|
|
}
|
|
} else {
|
|
console.error('无法获取选中文本')
|
|
}
|
|
}
|
|
},
|
|
mounted() {
|
|
let that = this
|
|
this.$nextTick(() => {
|
|
if(this.activeContent) {
|
|
that.queryRating()
|
|
}
|
|
})
|
|
|
|
this.activeName = this.$store.state.read.activeName
|
|
|
|
|
|
addEventListener('keyup', function(event) {
|
|
//监听查找操作
|
|
if (event.shiftKey && 'F' === event.key.toUpperCase()) {
|
|
event.preventDefault()
|
|
that.showSearch = !that.showSearch
|
|
} else if ('ESCAPE' === event.key.toUpperCase()) {
|
|
that.showSearch = false
|
|
}
|
|
that.updateFind(that.showSearch)
|
|
}, true)
|
|
}
|
|
|
|
})
|
|
</script>
|
|
<style>
|
|
.heightlight {
|
|
color: #74f2ff;
|
|
font-size: larger;
|
|
}
|
|
|
|
.search-light {
|
|
color: blue;
|
|
}
|
|
|
|
.choose-search-text {
|
|
animation: changeshadow 1s ease-in infinite;
|
|
}
|
|
|
|
@keyframes changeshadow {
|
|
0% {
|
|
text-shadow: 0 0 4px blue
|
|
}
|
|
50% {
|
|
text-shadow: 0 0 40px blue
|
|
}
|
|
100% {
|
|
text-shadow: 0 0 4px blue
|
|
}
|
|
}
|
|
|
|
.choose-note {
|
|
cursor: pointer;
|
|
}
|
|
|
|
pre {
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.content {
|
|
height: 600px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.note-list {
|
|
height: 600px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.text {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.item {
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.clearfix::before,
|
|
.clearfix::after {
|
|
display: table;
|
|
content: "";
|
|
}
|
|
|
|
.clearfix::after {
|
|
clear: both
|
|
}
|
|
|
|
.star-button {
|
|
width: 100%;
|
|
}
|
|
|
|
.star-icon i {
|
|
font-size: 30px;
|
|
}
|
|
|
|
.w-100 {
|
|
width: 100%;
|
|
}
|
|
|
|
.el-input.el-input--prefix > input[type='text'] {
|
|
height: 40px;
|
|
}
|
|
|
|
.img {
|
|
height: 30px;
|
|
width: 30px;
|
|
vertical-align: text-top;
|
|
}
|
|
|
|
|
|
</style>
|
|
|