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.
 
 
 
cloudnote_web/pages/index/read.vue

772 lines
25 KiB

<template>
<div>
<v-contextmenu ref="contextmenu">
<v-contextmenu-item v-if="!disabled">
<el-popover
v-model="visible"
trigger="click"
>
<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('note.table.note_title')" prop="note_title">
<el-input v-model="note_form.note_title"
:placeholder="$t('input_please', { keyword: this.$t('note.table.note_title') })"></el-input>
</el-form-item>
<el-form-item :label="$t('note.table.note_content')" prop="note_content">
<el-input autosize type="textarea" v-model="note_form.note_content"
:placeholder="$t('input_please', { keyword: this.$t('note.table.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 :name="key" v-for="(item,key) in openList" :key="key">
<span slot="label" class="icon-size">
{{item.title}}
<el-tooltip :content="item.hasCollect?$t('read.tip.cancel_collect'):$t('read.tip.collect')">
<i @click="star($event,item)" :class="'el-icon-star-'+(item.hasCollect?'on':'off')"></i>
</el-tooltip>
</span>
<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="note-list ml3">
<template v-if="noteList.length>0">
<el-row v-for="(item,index) in noteList" :key="index" class="mb2">
<el-card :class="{'active_card':item.isActive}">
<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 size="mini" type="success" @click="download(item)"
icon="el-icon-download"></el-button>
</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>
<el-tooltip :content="$t('read.tip.click_note_list')" :disabled="item.isEdit">
<pre style="background-color: white">{{item.content}}</pre>
</el-tooltip>
</template>
</div>
</el-card>
<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('note.table.note_title') }),
trigger: 'blur'
}],
note_content: [{
required: true,
message: this.$t('input_please', { keyword: this.$t('note.table.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: {
//导出笔记
download(item) {
console.info('笔记内容\n'+JSON.stringify(item,null,2))
let ele = document.createElement('a')
//设置下载文件名
ele.download = `${this.$t('note.table.note_title')}.txt`
//隐藏元素
ele.style.display = 'none'
//字符内容转变成blob地址
let blob = new Blob([`${this.$t('note.table.note_title')}${item.title}
${this.$t('note.table.paper_name')}${this.activeContent.title}
${this.$t('note.table.original_text')}${item.originalText}
${this.$t('note.table.note_content')}${item.content}`])
//如果是链接这里也可以直接设置链接地址
ele.href = URL.createObjectURL(blob)
document.body.appendChild(ele)
//模拟点击
ele.click()
//移除元素
document.body.removeChild(ele)
},
//收藏
star(e, item) {
e.stopPropagation()
let that = this
this.GLOBAL.fetchJSON(`/v1/api/collect/${item.id}`, 'POST', {}, function(res) {
if (res.code === '200') {
that.$store.commit('read/updateCollect', item.title)
that.$message.info(that.$t(res.msg))
} else {
that.$message.error(that.$t(res.msg))
}
})
},
//查找评价记录
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) {
let that = this
this.GLOBAL.fetchJSON('/v1/api/notes/update', 'PUT', {
noteId: item.id,
noteContent: item.content
}, function(res) {
if (res.code === '200') {
that.$set(item, 'type', 'success')
that.$set(item, 'message', that.$t('read.tip.update_note_ok'))
} else {
that.$set(item, 'type', 'error')
that.$set(item, 'message', that.$t('read.tip.update_note_fail'))
}
})
},
//删除笔记
del(item: any, index: number) {
let that = this
let span = document.getElementById(item.id)
if (span) {
let text = document.createTextNode(span.innerText)
span.replaceWith(text)
that.pre.normalize()
this.GLOBAL.fetchJSON('/v1/api/notes/remove', 'DELETE', {
noteId: item.id,
paperId: that.activeContent.id,
content: that.pre.innerHTML
}, function(res) {
if (res.code === '200') {
that.noteList.splice(index, 1)
that.$store.commit('read/updateContent', that.pre.innerHTML)
} else {
that.$message.error(that.$t('error_500').toString())
}
})
} else {
that.$message.error(that.$t('error_500').toString())
}
},
//提交评分
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) {
this.noteList.forEach(i => {
if (i.id !== item.id) {
this.$set(i, 'isActive', false)
}
})
this.$set(item, 'isActive', true)
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.GLOBAL.fetchJSON('/v1/api/notes/add', 'POST', {
noteId: this.note_form.note_id,
noteTitle: this.note_form.note_title,
noteContent: this.note_form.note_content,
paperId: that.activeContent.id,
fileId: that.activeContent.fileId,
originalText: that.note_form.original_text,
content: that.pre.innerHTML
}, function(res) {
if (res.code === '200') {
that.$message.info({
message: that.$t('read.tip.add_tip_ok').toString(),
showClose: true,
duration: 1000,
onClose: function() {
that.$store.commit('read/updateContent', that.pre.innerHTML)
that.noteList.unshift({
id: that.note_form.note_id,
title: that.note_form.note_title,
content: that.note_form.note_content
})
that.resetNote()
}
})
} else {
that.$message.error(that.$t('error_500').toString())
}
})
} else {
that.$message.error(that.$t('read.tip.add_tip_fail').toString())
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('无法获取选中文本')
}
},
//查找笔记
findNote() {
let that = this
this.GLOBAL.fetchGet(`/v1/api/notes/list/${this.activeContent.id}`, {}, function(res) {
if (res.code === '200') {
that.noteList = []
res.data.data.forEach(item => {
let obj = {
id: item.noteId,
title: item.noteTitle,
content: item.noteContent,
isActive: item.noteId === that.$route.query.noteId,
originalText: item.originalText
}
console.info(`加载笔记内容:${JSON.stringify(obj,null,2)}`)
that.noteList.push(obj)
if (item.noteId === that.$route.query.noteId) {
that.jump(obj)
that.$nextTick(() => {
let ele = document.querySelector('div[class$=active_card]')
if (ele) {
ele.scrollIntoView({
block: 'center'
})
}
})
}
})
} else {
that.$message.error(that.$t('error_500').toString())
}
})
}
},
mounted() {
let that = this
this.$nextTick(() => {
if (this.activeContent) {
that.queryRating()
that.findNote()
}
})
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
}
if (that.activeContent) {
that.updateFind(that.showSearch)
}
}, true)
}
})
</script>
<style>
.heightlight {
color: #74f2ff;
font-size: larger;
}
.active_card {
border: 1px solid black;
}
.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;
}
.icon-size {
font-size: 28px;
}
</style>