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.

882 lines
25 KiB

<template>
<div class="page page-document">
<el-row :gutter="20">
<el-col :span="scaleSpan">
<el-card ref="docMain" shadow="never" class="doc-main">
<div slot="header" class="clearfix">
<h1>
1 year ago
<img :src="`/static/images/${document.icon}_24.png`" alt="" />
{{ document.title }}
</h1>
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item>
<nuxt-link to="/"
><i class="el-icon-s-home"></i> 首页</nuxt-link
>
</el-breadcrumb-item>
<el-breadcrumb-item
v-for="breadcrumb in breadcrumbs"
:key="'bread-' + breadcrumb.id"
>
<nuxt-link :to="`/category/${breadcrumb.id}`">{{
breadcrumb.title
}}</nuxt-link>
</el-breadcrumb-item>
<el-breadcrumb-item>文档阅览</el-breadcrumb-item>
</el-breadcrumb>
<div class="float-right doc-info">
<span
><i class="el-icon-files"></i>
{{ document.pages || '-' }} </span
>
<span
><i class="el-icon-download"></i>
{{ document.download_count || 0 }} 下载</span
>
<span
><i class="el-icon-view"></i>
{{ document.view_count || 0 }} 浏览</span
>
<span
><i class="el-icon-chat-dot-square"></i>
{{ document.comment_count || 0 }} 评论</span
>
<span
><i class="el-icon-star-off"></i>
{{ document.favorite_count || 0 }} 收藏</span
>
<span
><el-rate
v-model="document.score"
disabled
show-score
text-color="#ff9900"
score-template="{value}"
>
</el-rate
></span>
</div>
</div>
<template v-if="tips">
<el-alert
type="warning"
effect="dark"
:title="tips"
show-icon
:closable="false"
>
</el-alert>
<div class="mgt-20px"></div>
</template>
1 year ago
<div ref="docPages" class="doc-pages">
<el-image
v-for="(page, index) in pages"
:key="index + page.src"
1 year ago
:src="page.src"
:alt="page.alt"
lazy
class="doc-page"
1 year ago
:style="{
width: pageWidth + 'px',
height: pageHeight + 'px',
}"
>
</el-image>
</div>
<div class="doc-page-more text-center">
<div>下载文档到电脑方便使用</div>
<el-button
type="primary"
icon="el-icon-download"
:loading="downloading"
@click="downloadDocument"
>
下载文档({{ formatBytes(document.size) }})</el-button
>
1 year ago
<div v-if="document.preview - pages.length > 0">
还有 {{ document.preview - pages.length }} 页可预览
<span class="el-link el-link--primary" @click="continueRead"
>继续阅读</span
>
</div>
1 year ago
<div class="text-muted" v-else>
<small>- 可预览页数已预览完 -</small>
</div>
</div>
<div>
<div class="share-info">
本文档由
<nuxt-link
:to="`/user/${document.user_id}`"
class="el-link el-link--primary"
>{{ document.user.username || '匿名用户' }}</nuxt-link
>
<span class="text-muted">{{
formatDatetime(document.created_at)
}}</span>
上传分享
</div>
<div class="btn-actions">
<el-button
type="primary"
@click="showReport"
plain
icon="el-icon-warning-outline"
>举报</el-button
>
<el-button
type="primary"
icon="el-icon-download"
class="float-right"
:loading="downloading"
@click="downloadDocument"
>下载文档({{ formatBytes(document.size) }})</el-button
>
<el-button
v-if="favorite.id > 0"
type="primary"
plain
class="float-right"
icon="el-icon-star-on"
@click="deleteFavorite"
>取消收藏</el-button
>
<el-button
v-else
type="primary"
class="float-right"
icon="el-icon-star-off"
@click="createFavorite"
>收藏</el-button
>
</div>
</div>
</el-card>
1 year ago
<el-card
v-if="document.id > 0"
ref="commentBox"
shadow="never"
class="mgt-20px"
>
<div>
1 year ago
<span class="score-tips" v-if="disabledScore"> </span>
<span class="score-tips" v-else> </span>
<el-rate
:disabled="disabledScore"
v-model="score"
show-text
@change="setDocumentScore"
:texts="[
'该文档令人失望',
'该文档不怎么样',
'该文档一般般',
'该文档很让我满意',
'该文档非常棒',
]"
>
</el-rate>
</div>
<FormComment
:document-id="document.id"
@success="commentSuccess"
class="mgt-20px"
/>
1 year ago
<comment-list ref="commentList" :document-id="document.id" />
</el-card>
</el-col>
<el-col :span="24 - scaleSpan">
<el-card shadow="never">
<div slot="header">分享用户</div>
<user-card :hide-actions="true" :user="document.user" />
</el-card>
1 year ago
<el-card
shadow="never"
class="mgt-20px relate-docs"
ref="relateDocs"
1 year ago
v-if="relatedDocuments.length > 0"
>
<div slot="header">相关文档</div>
1 year ago
<document-simple-list :docs="relatedDocuments" />
</el-card>
</el-col>
</el-row>
<div class="fixed-buttons">
<el-card shadow="never">
<el-row>
<el-col :span="18">
<el-button-group class="btn-actions">
<el-tooltip content="全屏阅读">
<el-button
icon="el-icon-full-screen"
@click="fullscreen"
></el-button>
</el-tooltip>
<el-tooltip :content="favorite.id > 0 ? '取消收藏' : '收藏文档'">
<el-button
v-if="favorite.id > 0"
icon="el-icon-star-on"
@click="deleteFavorite"
></el-button>
<el-button
v-else
icon="el-icon-star-off"
@click="createFavorite"
></el-button>
</el-tooltip>
<el-tooltip content="缩小">
<el-button
icon="el-icon-zoom-out"
:disabled="scaleSpan === 18"
@click="zoomOut"
></el-button>
</el-tooltip>
<el-tooltip content="放大">
<el-button
icon="el-icon-zoom-in"
:disabled="scaleSpan === 24"
@click="zoomIn"
></el-button>
</el-tooltip>
<el-tooltip content="上一页">
<el-button
icon="el-icon-arrow-up"
:disabled="currentPage === 1"
@click="prevPage"
></el-button>
</el-tooltip>
<el-tooltip content="当前页数/总页数">
1 year ago
<el-button>{{ currentPage }}/{{ document.pages }}</el-button>
</el-tooltip>
<el-tooltip content="下一页">
<el-button
icon="el-icon-arrow-down"
:disabled="currentPage === document.preview"
@click="nextPage"
></el-button>
</el-tooltip>
</el-button-group>
1 year ago
<el-button
class="btn-comment"
icon="el-icon-chat-dot-square"
@click="gotoComment"
>文档点评</el-button
>
<el-button-group class="float-right">
<el-button type="primary" icon="el-icon-coin" class="btn-coin"
1 year ago
>{{ document.price || 0 }} 个魔豆</el-button
>
<el-button
type="primary"
icon="el-icon-download"
:loading="downloading"
@click="downloadDocument"
>下载文档({{ formatBytes(document.size) }})</el-button
>
</el-button-group>
</el-col>
<el-col :span="6" class="text-right">
<el-button icon="el-icon-top" @click="scrollTop"
>回到顶部</el-button
>
</el-col>
</el-row>
</el-card>
</div>
<el-dialog title="举报文档" :visible.sync="reportVisible" width="520px">
<FormReport
ref="reportForm"
:init-report="report"
:is-admin="false"
@success="formReportSuccess"
/>
</el-dialog>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import DocumentSimpleList from '~/components/DocumentSimpleList.vue'
1 year ago
import {
getDocument,
downloadDocument,
getRelatedDocuments,
setDocumentScore,
getDocumentScore,
1 year ago
} from '~/api/document'
import { getFavorite, createFavorite, deleteFavorite } from '~/api/favorite'
1 year ago
import { formatDatetime, formatBytes, getIcon } from '~/utils/utils'
import { documentStatusOptions } from '~/utils/enum'
1 year ago
import FormComment from '~/components/FormComment.vue'
import CommentList from '~/components/CommentList.vue'
export default {
1 year ago
components: { DocumentSimpleList, FormComment, CommentList },
data() {
return {
documentStatusOptions,
docs: [],
user: {
id: 0,
},
document: {
id: 0,
score: 4.0,
user: {
id: 0,
},
attachment: {
hash: '',
},
},
score: null,
disabledScore: false,
downloading: false,
documentId: parseInt(this.$route.params.id) || 0,
pages: [],
1 year ago
pageHeight: 0,
pageWidth: 0,
currentPage: 1,
currentPageFullscreen: 1,
breadcrumbs: [],
favorite: {
id: 0,
},
scaleSpan: 18,
loadingImage: '/static/images/loading.svg',
reportVisible: false,
report: {
document_id: 0,
document_title: '',
reason: 1,
},
1 year ago
relatedDocuments: [],
cardWidth: 0,
cardOffsetTop: 0,
tips: '',
}
},
head() {
return {
1 year ago
title: this.document.title + ' - ' + this.settings.system.sitename,
meta: [
{
hid: 'description',
name: 'description',
content: this.document.description,
},
{
hid: 'keywords',
name: 'keywords',
content: this.document.keywords,
},
],
}
},
computed: {
...mapGetters('category', ['categoryMap']),
1 year ago
...mapGetters('setting', ['settings']),
},
created() {
1 year ago
Promise.all([
this.getDocument(),
this.getFavorite(),
this.getRelatedDocuments(),
this.getDocumentScore(),
1 year ago
])
},
1 year ago
mounted() {
window.addEventListener('scroll', this.handleScroll)
try {
this.$refs.docMain.$el.addEventListener(
'scroll',
this.handleFullscreenScroll
)
} catch (error) {
console.log(error)
}
window.addEventListener('fullscreenchange', this.fullscreenchange)
1 year ago
},
beforeDestroy() {
window.removeEventListener('scroll', this.handleScroll)
try {
this.$refs.docMain.$el.removeEventListener(
'scroll',
this.handleFullscreenScroll
)
} catch (error) {
console.log(error)
}
window.removeEventListener('fullscreenchange', this.fullscreenchange)
},
methods: {
formatDatetime,
formatBytes,
...mapActions('user', ['getUser']),
async getDocument() {
const res = await getDocument({
id: this.documentId,
with_author: true,
})
if (res.status === 200) {
const doc = res.data || {}
doc.score = parseFloat(doc.score) / 100 || 4.0
if (!doc.preview || doc.preview >= doc.pages) {
doc.preview = doc.pages
}
1 year ago
// 限定每次预览页数
let preview = 2
if (doc.preview < preview) {
preview = doc.preview
}
// 限定预览页数,拼装图片链接
const pages = []
1 year ago
for (let i = 1; i <= preview; i++) {
1 year ago
const src = doc.enable_gzip
? `/view/page/${doc.attachment.hash}/${i}.gzip.svg`
: `/view/page/${doc.attachment.hash}/${i}.svg`
1 year ago
pages.push({
1 year ago
lazySrc: src,
src: src,
1 year ago
alt: `${doc.title}${i + 1}`,
})
}
this.breadcrumbs = (doc.category_id || []).map((id) => {
return this.categoryMap[id]
})
1 year ago
doc.icon = getIcon(doc.ext)
this.pages = pages
this.document = doc
1 year ago
this.pageWidth = this.$refs.docPages.offsetWidth
this.pageHeight =
(this.$refs.docPages.offsetWidth / doc.width) * doc.height
if (doc.status !== 2) {
// 2 为文档已转换成功,不需要展示提示
this.documentStatusOptions.map((item) => {
if (item.value === doc.status) {
this.tips = `当前文档【${item.label}】,可能暂时无法正常提供预览,建议您下载到本地进行阅读。`
}
})
}
} else {
this.$message.error(res.data.message)
1 year ago
this.$router.replace('/404')
}
},
showReport() {
this.report.document_id = this.document.id
this.report.document_title = this.document.title
this.reportVisible = true
},
formReportSuccess() {
this.reportVisible = false
},
1 year ago
handleScroll() {
const scrollTop =
document.documentElement.scrollTop || document.body.scrollTop
// 还有5像素的border
let currentPage = Math.round(scrollTop / (this.pageHeight + 5)) + 1
1 year ago
if (currentPage > this.pages.length) {
currentPage = this.pages.length
}
this.currentPage = currentPage
1 year ago
this.pages[currentPage - 1].src = this.pages[currentPage - 1].lazySrc
1 year ago
// 右侧相关文档固定
try {
const relateDocs = this.$refs.relateDocs.$el
if (relateDocs) {
if (this.cardWidth === 0) {
this.cardWidth = relateDocs.offsetWidth
this.cardOffsetTop = relateDocs.offsetTop
}
1 year ago
if (scrollTop > this.cardOffsetTop) {
relateDocs.style.position = 'fixed'
relateDocs.style.top = '60px'
relateDocs.style.zIndex = '999'
relateDocs.style.width = `${this.cardWidth}px`
} else {
relateDocs.style = null
}
}
1 year ago
} catch (error) {
console.log('handleScroll relateDocs', error)
}
1 year ago
},
handleFullscreenScroll() {
try {
const scrollTop = this.$refs.docMain.$el.scrollTop
if (scrollTop === 0) {
// 当退出全屏的时候会触发这个事件但是scrollTop为0所以直接返回避免直接将当前页码重置为1
return
}
let currentPage = Math.round(scrollTop / (this.pageHeight + 5)) + 1
if (currentPage > this.pages.length) {
currentPage = this.pages.length
}
this.currentPageFullscreen = currentPage
} catch (error) {
console.log(error)
}
},
scrollTop() {
this.scrollTo(0)
},
1 year ago
gotoComment() {
try {
this.scrollTo(this.$refs.commentBox.$el.offsetTop)
} catch (error) {
console.log('gotoComment', error)
}
},
commentSuccess() {
this.$refs.commentList.getComments()
},
async downloadDocument() {
this.downloading = true
const res = await downloadDocument({
id: this.documentId,
})
if (res.status === 200) {
this.getUser()
// 跳转下载
window.location.href = res.data.url
} else {
this.$message.error(res.data.message || '下载失败')
}
this.downloading = false
},
1 year ago
async getRelatedDocuments() {
const res = await getRelatedDocuments({
id: this.documentId,
})
if (res.status === 200) {
this.relatedDocuments = res.data.document || []
}
},
prevPage() {
if (this.currentPage > 1) {
const currentPage = this.currentPage - 1
this.scrollToPage(currentPage)
}
},
nextPage() {
if (this.currentPage < this.document.preview) {
const currentPage = this.currentPage + 1
if (currentPage > this.pages.length) {
this.continueRead()
}
this.scrollToPage(currentPage)
}
},
scrollToPage(page) {
const scrollTop = (page - 1) * this.pageHeight
this.scrollTo(scrollTop)
},
scrollTo(position) {
document.scrollingElement.scrollTo({
top: position,
behavior: 'smooth',
})
this.$refs.docMain.$el.scrollTo({
top: position,
behavior: 'smooth',
})
},
getDocMainWidth() {
return this.$refs.docMain.$el.offsetWidth
},
// 缩小
zoomOut() {
if (this.scaleSpan > 18) {
const currentPage = this.currentPage
1 year ago
this.scaleSpan -= 6
this.$nextTick(() => {
this.zoomSetPage(currentPage)
})
}
},
// 放大
zoomIn() {
if (this.scaleSpan < 24) {
const currentPage = this.currentPage
1 year ago
this.scaleSpan += 6
this.$nextTick(() => {
this.zoomSetPage(currentPage)
})
}
},
zoomSetPage(page) {
const newPageWidth = this.getDocMainWidth() - 20 * 2 // 减去两个内边距因为设置了border-box所以两个border的宽度不计
const newPageHeight = (newPageWidth / this.pageWidth) * this.pageHeight
this.pageWidth = newPageWidth
this.pageHeight = newPageHeight
this.$nextTick(() => {
this.scrollToPage(page)
})
},
// 全屏
fullscreen() {
// 全屏前,将当前浏览的页码赋值到全屏时浏览的页码
this.currentPageFullscreen = this.currentPage
const docPages = this.$refs.docMain.$el
if (docPages.requestFullscreen) {
docPages.requestFullscreen()
} else if (docPages.mozRequestFullScreen) {
docPages.mozRequestFullScreen()
} else if (docPages.webkitRequestFullscreen) {
docPages.webkitRequestFullscreen()
} else if (docPages.msRequestFullscreen) {
docPages.msRequestFullscreen()
}
},
fullscreenchange(e) {
const currentPage = this.currentPageFullscreen
console.log('fullscreenchange currentPage', currentPage)
if (document.fullscreenElement) {
// 全屏
this.scaleSpan = 24
this.pages.map((page) => {
page.src = page.lazySrc
return page
})
} else {
this.scaleSpan = 18
}
this.$nextTick(() => {
this.zoomSetPage(currentPage)
})
},
async getFavorite() {
const res = await getFavorite({
document_id: this.documentId,
})
if (res.status === 200) {
this.favorite = res.data || { id: 0 }
}
},
// 取消收藏
async deleteFavorite() {
const res = await deleteFavorite({ id: this.favorite.id })
if (res.status === 200) {
this.$message.success('取消收藏成功')
this.favorite = { id: 0 }
} else {
this.$message.error(res.data.message)
}
},
// 添加收藏
async createFavorite() {
const res = await createFavorite({
document_id: this.documentId,
})
if (res.status === 200) {
this.$message.success('收藏成功')
this.favorite = res.data
} else {
this.$message.error(res.data.message)
}
},
1 year ago
continueRead() {
let end = this.pages.length + 5
if (end > this.document.preview) {
end = this.document.preview
}
let j = 0
let startLazyLoad = 2
if (document.fullscreenElement) startLazyLoad = 5
1 year ago
for (let i = this.pages.length + 1; i <= end; i++) {
j += 1
const src = this.document.enable_gzip
1 year ago
? `/view/page/${this.document.attachment.hash}/${i}.gzip.svg`
: `/view/page/${this.document.attachment.hash}/${i}.svg`
1 year ago
this.pages.push({
// 前两页,直接不要懒加载,如果非全屏
src: j <= startLazyLoad ? src : this.loadingImage,
lazySrc: src,
1 year ago
alt: `${this.document.title}${i + 1}`,
})
}
},
async setDocumentScore() {
if (!this.score) {
return
}
const res = await setDocumentScore({
document_id: this.documentId,
score: this.score * 100,
})
if (res.status === 200) {
this.$message.success('提交评分成功')
this.disabledScore = true
} else {
this.$message.error(res.data.message)
}
},
async getDocumentScore() {
// 判断用户是否已登录
let userId = 0
try {
userId = this.$store.state.user.user.id || 0
} catch (error) {}
if (!userId) {
return
}
const res = await getDocumentScore({
document_id: this.documentId,
})
if (res.status === 200) {
const score = res.data.score / 100 || null
this.score = score
if (score) this.disabledScore = true
} else {
this.$message.error(res.data.message)
}
},
},
}
</script>
<style lang="scss">
.page-document {
.doc-main {
overflow: auto;
}
.relate-docs {
.el-card__body {
padding-top: 10px;
}
}
h1 {
margin: 0;
img {
position: relative;
top: 3px;
}
}
.el-breadcrumb {
font-weight: normal;
margin-top: 12px;
color: #565656;
.el-breadcrumb__inner a,
.el-breadcrumb__inner.is-link {
font-weight: normal;
}
.el-breadcrumb__separator[class*='icon'] {
margin: 0 3px;
}
.el-breadcrumb__inner {
color: #666;
}
}
.doc-info {
font-weight: normal;
position: relative;
top: -16px;
font-size: 14px;
color: #bbb;
& > span {
margin-left: 8px;
}
.el-rate {
position: relative;
top: -2px;
}
}
.doc-pages {
.doc-page {
1 year ago
display: block;
width: 100%;
box-sizing: border-box;
border: 5px solid $background-grey-light;
border-bottom: 0;
&:last-child {
border-bottom: 5px solid $background-grey-light;
}
img {
width: 100%;
1 year ago
background-color: #fff;
}
}
}
.doc-page-more {
padding: 30px 0;
border: 5px solid $background-grey-light;
border-top: 0;
color: #565656;
.el-button {
margin: 10px 0;
}
}
.share-info {
font-size: 15px;
color: #666;
margin: 15px 0;
.el-link {
top: -2px;
}
}
.fixed-buttons {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
width: 100%;
min-width: $min-width;
background-color: #ecf0f1;
height: 50px;
[class*=' el-icon-'],
[class^='el-icon-'] {
font-weight: bold;
}
.el-card {
border-radius: 0;
background-color: transparent;
width: $default-width;
max-width: $max-width;
margin: 0 auto;
.el-card__body {
padding: 0;
}
.el-button {
border: 0;
border-radius: 0;
padding: 18px 20px;
}
.btn-comment {
top: 1px;
position: relative;
background-color: transparent;
&:hover {
background-color: #ecf5ff;
}
}
.btn-actions .el-button {
background-color: transparent;
&:hover {
background-color: #ecf5ff;
}
}
.btn-coin {
background-color: transparent;
color: #606266;
cursor: auto;
}
}
}
.score-tips {
position: relative;
top: 3px;
margin-right: 10px;
color: #565656;
}
}
</style>