前端项目初始化提交
This commit is contained in:
779
src/components/pdfReader/AdvancedPDFReader.vue
Normal file
779
src/components/pdfReader/AdvancedPDFReader.vue
Normal file
@@ -0,0 +1,779 @@
|
||||
<template>
|
||||
<div class="pdf-reader-container" :class="className">
|
||||
<!-- 主阅读区域 - 更宽的展示空间 -->
|
||||
<div class="pdf-reader-content">
|
||||
<!-- PDF内容展示 -->
|
||||
<div class="pdf-viewer-container">
|
||||
<PDFPage
|
||||
:page-number="currentPage"
|
||||
:scale="scale"
|
||||
:highlights="showHighlights ? annotations.highlights : []"
|
||||
:selected-highlight-id="selectedHighlight?.id || null"
|
||||
:on-highlight-click="handleHighlightClick"
|
||||
:on-add-highlight="handleAddHighlight"
|
||||
:on-text-content="handleTextContent"
|
||||
:read-only="readOnly"
|
||||
:pdf-source="file"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 右侧批注面板 - 文档视图分离 -->
|
||||
<div v-if="showSidebar" class="annotation-sidebar">
|
||||
<div class="annotation-sidebar-header">
|
||||
<h4 class="annotation-sidebar-title">批注管理</h4>
|
||||
<button
|
||||
@click="showSidebar = false"
|
||||
class="annotation-sidebar-close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="annotation-sidebar-content">
|
||||
<!-- 高亮批注列表 -->
|
||||
<div v-if="annotations.highlights.length > 0" class="annotation-section">
|
||||
<div class="annotation-section-title">高亮批注</div>
|
||||
<div class="annotation-list">
|
||||
<div
|
||||
v-for="highlight in annotations.highlights"
|
||||
:key="highlight.id"
|
||||
class="annotation-item"
|
||||
:style="{ borderLeft: `3px solid ${highlight.color}` }"
|
||||
@click="selectHighlight(highlight)"
|
||||
>
|
||||
<div class="annotation-item-content">
|
||||
<div class="annotation-item-text">
|
||||
{{ highlight.content?.substring(0, 50) }}{{ highlight.content?.length > 50 ? '...' : '' }}
|
||||
</div>
|
||||
<div class="annotation-item-meta">
|
||||
<span>第{{ highlight.pageNumber }}页</span>
|
||||
<button
|
||||
@click.stop="handleDeleteHighlight(highlight.id)"
|
||||
class="annotation-delete-btn"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关联的评论 -->
|
||||
<div v-if="getHighlightComments(highlight.id).length > 0" class="annotation-comments">
|
||||
<div
|
||||
v-for="comment in getHighlightComments(highlight.id)"
|
||||
:key="comment.id"
|
||||
class="annotation-comment-item"
|
||||
>
|
||||
<div class="annotation-comment-text">{{ comment.text }}</div>
|
||||
<div class="annotation-comment-meta">
|
||||
{{ new Date(comment.createdAt).toLocaleString() }}
|
||||
<button
|
||||
@click.stop="handleDeleteComment(comment.id)"
|
||||
class="annotation-delete-btn"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 独立评论列表 -->
|
||||
<div v-if="independentComments.length > 0" class="annotation-section">
|
||||
<div class="annotation-section-title">独立评论</div>
|
||||
<div class="annotation-list">
|
||||
<div
|
||||
v-for="comment in independentComments"
|
||||
:key="comment.id"
|
||||
class="annotation-item"
|
||||
@click="goToCommentPage(comment.pageNumber)"
|
||||
>
|
||||
<div class="annotation-item-content">
|
||||
<div class="annotation-item-text">{{ comment.text }}</div>
|
||||
<div class="annotation-item-meta">
|
||||
<span>第{{ comment.pageNumber || '未知' }}页</span>
|
||||
<button
|
||||
@click.stop="handleDeleteComment(comment.id)"
|
||||
class="annotation-delete-btn"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="annotations.highlights.length === 0 && independentComments.length === 0" class="annotation-empty">
|
||||
<p>暂无批注</p>
|
||||
<p class="annotation-empty-text">选择文本添加高亮或批注</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批注弹窗 -->
|
||||
<CommentPopup
|
||||
v-if="showCommentPopup && selectedHighlight"
|
||||
:comments="getHighlightComments(selectedHighlight.id)"
|
||||
:position="commentPopupPosition"
|
||||
:on-add-comment="handleAddComment"
|
||||
:on-delete-comment="handleDeleteComment"
|
||||
:on-close="() => {
|
||||
showCommentPopup = false
|
||||
selectedHighlight = null
|
||||
}"
|
||||
:on-delete-highlight="handleDeleteHighlight"
|
||||
:highlight-content="selectedHighlight.content"
|
||||
/>
|
||||
|
||||
<!-- 操作按钮(朗读、MinerU提取等) -->
|
||||
<div v-if="showCommentPopup && selectedHighlight?.content" class="action-buttons-container">
|
||||
<button
|
||||
@click="handleSpeak"
|
||||
class="action-button"
|
||||
:disabled="!isSpeechAvailable"
|
||||
>
|
||||
{{ isSpeaking ? '停止朗读' : '朗读' }}
|
||||
</button>
|
||||
<button
|
||||
@click="() => handleMineruExtract(selectedHighlight.pageNumber)"
|
||||
class="action-button action-button-success"
|
||||
:disabled="isMineruProcessing"
|
||||
>
|
||||
{{ isMineruProcessing ? '提取中...' : 'MinerU提取' }}
|
||||
</button>
|
||||
<div v-if="mineruError" class="error-message error-message-danger">
|
||||
{{ mineruError }}
|
||||
</div>
|
||||
<div v-if="mineruExtractedContent[selectedHighlight.pageNumber]" class="error-message error-message-success">
|
||||
<strong>MinerU提取结果:</strong>
|
||||
<p>{{ mineruExtractedContent[selectedHighlight.pageNumber].substring(0, 100) }}...</p>
|
||||
</div>
|
||||
<div v-if="errorMessage" class="error-message error-message-danger">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 侧边栏切换按钮 -->
|
||||
<button
|
||||
@click="showSidebar = !showSidebar"
|
||||
class="sidebar-toggle-button"
|
||||
>
|
||||
{{ showSidebar ? '收起批注' : '显示批注' }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import CommentPopup from './CommentPopup.vue'
|
||||
import PDFPage from './PDFPage.vue'
|
||||
import {
|
||||
createAnnotationData,
|
||||
addHighlight,
|
||||
removeHighlight,
|
||||
addComment,
|
||||
removeComment,
|
||||
exportAnnotations,
|
||||
importAnnotations
|
||||
} from '../../utils/annotationUtils'
|
||||
import { useSpeechSynthesis } from '../../hooks/useSpeechSynthesis'
|
||||
|
||||
const props = defineProps({
|
||||
file: {
|
||||
type: [String, Object, ArrayBuffer],
|
||||
required: true
|
||||
},
|
||||
initialAnnotations: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
onAnnotationsChange: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
currentMode: {
|
||||
type: String,
|
||||
default: 'select'
|
||||
},
|
||||
selectedColor: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
numPages: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
scale: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
onPageChange: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onScaleChange: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onNumPagesChange: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onHighlightClick: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
showHighlights: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isReading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
readingSpeed: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
readingRange: {
|
||||
type: String,
|
||||
default: 'current'
|
||||
},
|
||||
onReadingStatusChange: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onReadingProgressChange: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onReadingTextChange: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const migrateInitialAnnotations = (data) => {
|
||||
if (!data) return createAnnotationData()
|
||||
|
||||
const migratedHighlights = data.highlights?.map((h) => {
|
||||
if ('positions' in h) {
|
||||
return h
|
||||
}
|
||||
if ('position' in h) {
|
||||
return {
|
||||
...h,
|
||||
positions: [h.position]
|
||||
}
|
||||
}
|
||||
return h
|
||||
}) || []
|
||||
|
||||
return {
|
||||
...data,
|
||||
highlights: migratedHighlights,
|
||||
version: '2.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
const numPages = ref(props.numPages || 0)
|
||||
const currentPage = ref(props.currentPage || 1)
|
||||
const scale = ref(props.scale || 1)
|
||||
const annotations = ref(migrateInitialAnnotations(props.initialAnnotations))
|
||||
const selectedHighlight = ref(null)
|
||||
const commentPopupPosition = reactive({ x: 0, y: 0 })
|
||||
const showCommentPopup = ref(false)
|
||||
const showSidebar = ref(false)
|
||||
const showControls = ref(false)
|
||||
const pageTextContent = reactive({})
|
||||
const mineruExtractedContent = reactive({})
|
||||
const isMineruProcessing = ref(false)
|
||||
const mineruError = ref(null)
|
||||
|
||||
// 计算属性:独立评论(没有关联高亮的评论)
|
||||
const independentComments = computed(() => {
|
||||
return annotations.value.comments.filter(comment => !comment.highlightId)
|
||||
})
|
||||
|
||||
// 选择高亮并跳转到对应页面
|
||||
const selectHighlight = (highlight) => {
|
||||
selectedHighlight.value = highlight
|
||||
currentPage.value = highlight.pageNumber
|
||||
if (props.onPageChange) {
|
||||
props.onPageChange(highlight.pageNumber)
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到评论所在页面
|
||||
const goToCommentPage = (pageNumber) => {
|
||||
if (pageNumber) {
|
||||
currentPage.value = pageNumber
|
||||
if (props.onPageChange) {
|
||||
props.onPageChange(pageNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按ID删除高亮
|
||||
const handleDeleteHighlight = (highlightId) => {
|
||||
const highlightToDelete = highlightId || (selectedHighlight.value?.id)
|
||||
if (highlightToDelete) {
|
||||
updateAnnotations(removeHighlight(annotations.value, highlightToDelete))
|
||||
showCommentPopup.value = false
|
||||
selectedHighlight.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
speak,
|
||||
pause,
|
||||
resume,
|
||||
stop,
|
||||
isSpeaking,
|
||||
isPaused,
|
||||
speakingProgress,
|
||||
setRate,
|
||||
setVolume,
|
||||
isSpeechAvailable,
|
||||
errorMessage
|
||||
} = useSpeechSynthesis()
|
||||
|
||||
watch(() => props.currentPage, (newPage) => {
|
||||
if (newPage !== undefined) {
|
||||
currentPage.value = newPage
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.numPages, (newPages) => {
|
||||
if (newPages !== undefined) {
|
||||
numPages.value = newPages
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.scale, (newScale) => {
|
||||
if (newScale !== undefined) {
|
||||
scale.value = newScale
|
||||
}
|
||||
})
|
||||
|
||||
const updateAnnotations = (newData) => {
|
||||
annotations.value = newData
|
||||
if (props.onAnnotationsChange) {
|
||||
props.onAnnotationsChange(newData)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTextContent = (pageNum, textContent) => {
|
||||
pageTextContent[pageNum] = textContent
|
||||
}
|
||||
|
||||
const extractTextFromPage = (pageNum) => {
|
||||
const content = pageTextContent[pageNum]
|
||||
if (!content) return ''
|
||||
|
||||
return content.items.map(item => item.str).join(' ')
|
||||
}
|
||||
|
||||
const handleReadingToggle = () => {
|
||||
console.log('handleReadingToggle called, isSpeaking:', isSpeaking.value)
|
||||
if (isSpeaking.value) {
|
||||
stop()
|
||||
if (props.onReadingStatusChange) {
|
||||
props.onReadingStatusChange(false)
|
||||
}
|
||||
} else {
|
||||
let text = ''
|
||||
let description = ''
|
||||
|
||||
console.log('Current page:', currentPage.value)
|
||||
console.log('Reading range:', props.readingRange)
|
||||
|
||||
if (props.readingRange === 'selected') {
|
||||
if (selectedHighlight.value?.content) {
|
||||
text = selectedHighlight.value.content
|
||||
description = '选中文本'
|
||||
} else {
|
||||
console.log('No text selected')
|
||||
if (props.onReadingTextChange) {
|
||||
props.onReadingTextChange('请先选择要朗读的文本')
|
||||
}
|
||||
return
|
||||
}
|
||||
} else if (props.readingRange === 'current') {
|
||||
text = extractTextFromPage(currentPage.value)
|
||||
description = `第${currentPage.value}页`
|
||||
} else if (props.readingRange === 'chapter') {
|
||||
console.log('Page text content:', pageTextContent)
|
||||
const chapterPages = []
|
||||
for (let i = currentPage.value; i <= Math.min(currentPage.value + 8, numPages.value); i++) {
|
||||
chapterPages.push(i)
|
||||
}
|
||||
chapterPages.forEach(pageNum => {
|
||||
text += extractTextFromPage(pageNum) + ' '
|
||||
})
|
||||
description = `第${currentPage.value}-${Math.min(currentPage.value + 8, numPages.value)}页`
|
||||
} else if (props.readingRange === 'full') {
|
||||
console.log('Page text content:', pageTextContent)
|
||||
for (let i = 1; i <= numPages.value; i++) {
|
||||
text += extractTextFromPage(i) + ' '
|
||||
}
|
||||
description = `全文(共${numPages.value}页)`
|
||||
}
|
||||
|
||||
console.log('Extracted text:', text)
|
||||
console.log('Description:', description)
|
||||
|
||||
if (text && text.trim().length > 0) {
|
||||
if (props.onReadingTextChange) {
|
||||
props.onReadingTextChange(`正在朗读${description}`)
|
||||
}
|
||||
console.log('Calling speak with text length:', text.length)
|
||||
console.log('First 100 characters:', text.substring(0, 100))
|
||||
|
||||
const maxTextLength = 3000
|
||||
const speakingText = text.length > maxTextLength ? text.substring(0, maxTextLength) + '...' : text
|
||||
|
||||
speak(speakingText, {
|
||||
rate: props.readingSpeed,
|
||||
onProgress: (progress) => {
|
||||
console.log('Reading progress:', progress)
|
||||
if (props.onReadingProgressChange) {
|
||||
props.onReadingProgressChange(progress)
|
||||
}
|
||||
}
|
||||
})
|
||||
if (props.onReadingStatusChange) {
|
||||
props.onReadingStatusChange(true)
|
||||
}
|
||||
} else {
|
||||
console.log('No text extracted, cannot speak')
|
||||
if (props.onReadingTextChange) {
|
||||
props.onReadingTextChange('无法提取文本,请检查PDF文件')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch([() => props.isReading, isSpeaking], ([newIsReading, newIsSpeaking]) => {
|
||||
if (newIsReading && !newIsSpeaking) {
|
||||
handleReadingToggle()
|
||||
} else if (!newIsReading && newIsSpeaking) {
|
||||
stop()
|
||||
if (props.onReadingStatusChange) {
|
||||
props.onReadingStatusChange(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleReadingPause = () => {
|
||||
if (isSpeaking.value) {
|
||||
pause()
|
||||
} else if (isPaused.value) {
|
||||
resume()
|
||||
}
|
||||
}
|
||||
|
||||
const handleReadingStop = () => {
|
||||
stop()
|
||||
}
|
||||
|
||||
const handleZoomIn = () => {
|
||||
const newScale = Math.min(scale.value + 0.25, 3)
|
||||
scale.value = newScale
|
||||
if (props.onScaleChange) {
|
||||
props.onScaleChange(newScale)
|
||||
}
|
||||
}
|
||||
|
||||
const handleZoomOut = () => {
|
||||
const newScale = Math.max(scale.value - 0.25, 0.5)
|
||||
scale.value = newScale
|
||||
if (props.onScaleChange) {
|
||||
props.onScaleChange(newScale)
|
||||
}
|
||||
}
|
||||
|
||||
const handleZoomReset = () => {
|
||||
scale.value = 1
|
||||
if (props.onScaleChange) {
|
||||
props.onScaleChange(1)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePreviousPage = () => {
|
||||
const newPage = Math.max(currentPage.value - 1, 1)
|
||||
currentPage.value = newPage
|
||||
if (props.onPageChange) {
|
||||
props.onPageChange(newPage)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNextPage = () => {
|
||||
const newPage = Math.min(currentPage.value + 1, numPages.value)
|
||||
currentPage.value = newPage
|
||||
if (props.onPageChange) {
|
||||
props.onPageChange(newPage)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
const newPage = parseInt(page)
|
||||
if (!isNaN(newPage) && newPage >= 1 && newPage <= numPages.value) {
|
||||
currentPage.value = newPage
|
||||
if (props.onPageChange) {
|
||||
props.onPageChange(newPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddHighlight = (highlight) => {
|
||||
if (props.readOnly) return
|
||||
|
||||
if (props.currentMode === 'highlight') {
|
||||
const highlightWithColor = {
|
||||
...highlight,
|
||||
color: props.selectedColor || '#ffeb3b'
|
||||
}
|
||||
updateAnnotations(addHighlight(annotations.value, highlightWithColor))
|
||||
} else if (props.currentMode === 'comment') {
|
||||
selectedHighlight.value = highlight
|
||||
const position = highlight.screenPosition || { x: window.innerWidth / 2, y: window.innerHeight / 2 }
|
||||
const adjustedX = Math.max(20, Math.min(position.x, window.innerWidth - 320))
|
||||
const adjustedY = Math.max(20, Math.min(position.y, window.innerHeight - 500))
|
||||
commentPopupPosition.x = adjustedX
|
||||
commentPopupPosition.y = adjustedY
|
||||
showCommentPopup.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const handleHighlightClick = (highlight, e) => {
|
||||
selectedHighlight.value = highlight
|
||||
commentPopupPosition.x = e.clientX
|
||||
commentPopupPosition.y = e.clientY
|
||||
showCommentPopup.value = true
|
||||
if (props.onHighlightClick) {
|
||||
props.onHighlightClick(highlight)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddComment = (text, shouldAddHighlight = false, highlightColor = '#ffeb3b') => {
|
||||
let newAnnotations = annotations.value
|
||||
let highlightId = null
|
||||
|
||||
if (shouldAddHighlight && selectedHighlight.value) {
|
||||
const highlightWithColor = {
|
||||
...selectedHighlight.value,
|
||||
color: highlightColor
|
||||
}
|
||||
newAnnotations = addHighlight(newAnnotations, highlightWithColor)
|
||||
highlightId = newAnnotations.highlights[newAnnotations.highlights.length - 1].id
|
||||
} else if (selectedHighlight.value && selectedHighlight.value.id) {
|
||||
highlightId = selectedHighlight.value.id
|
||||
}
|
||||
|
||||
if (highlightId) {
|
||||
const updatedAnnotations = addComment(newAnnotations, highlightId, text, selectedHighlight.value?.pageNumber, selectedHighlight.value?.content)
|
||||
updateAnnotations(updatedAnnotations)
|
||||
} else {
|
||||
const updatedAnnotations = addComment(newAnnotations, null, text, currentPage.value, null)
|
||||
updateAnnotations(updatedAnnotations)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteComment = (commentId) => {
|
||||
updateAnnotations(removeComment(annotations.value, commentId))
|
||||
}
|
||||
|
||||
|
||||
|
||||
const handleExport = () => {
|
||||
exportAnnotations(annotations.value)
|
||||
}
|
||||
|
||||
const handleImport = async (file) => {
|
||||
try {
|
||||
const data = await importAnnotations(file)
|
||||
updateAnnotations(data)
|
||||
} catch (err) {
|
||||
console.error('Failed to import annotations:', err)
|
||||
alert('导入批注失败,请检查文件格式')
|
||||
}
|
||||
}
|
||||
|
||||
const handleMineruExtract = async (pageNumber) => {
|
||||
isMineruProcessing.value = true
|
||||
mineruError.value = null
|
||||
|
||||
try {
|
||||
console.log('开始使用MinerU提取第', pageNumber, '页内容')
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
const mockContent = `这是使用MinerU提取的第${pageNumber}页内容。\n\nMinerU能够智能识别PDF文档的结构,包括标题、段落、表格等元素,并将其转换为结构化的文本格式。\n\n这种方式比传统的OCR更加准确,能够更好地保留文档的原始结构和格式。`
|
||||
|
||||
mineruExtractedContent[pageNumber] = mockContent
|
||||
|
||||
console.log('MinerU提取完成:', mockContent)
|
||||
} catch (error) {
|
||||
console.error('MinerU提取失败:', error)
|
||||
mineruError.value = '内容提取失败,请检查MinerU服务是否运行'
|
||||
} finally {
|
||||
isMineruProcessing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSpeak = () => {
|
||||
if (selectedHighlight.value?.content) {
|
||||
console.log('Speaking selected highlight content:', selectedHighlight.value.content)
|
||||
speak(selectedHighlight.value.content, {
|
||||
onProgress: (progress) => {
|
||||
console.log('Selected text reading progress:', progress)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const getHighlightComments = (highlightId) => {
|
||||
return annotations.value.comments.filter(c => c.highlightId === highlightId)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
handleZoomIn,
|
||||
handleZoomOut,
|
||||
handleZoomReset,
|
||||
handlePreviousPage,
|
||||
handleNextPage,
|
||||
handlePageChange,
|
||||
handleReadingToggle,
|
||||
handleReadingPause,
|
||||
handleReadingStop,
|
||||
handleExport,
|
||||
handleImport
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import './pdfReader.css';
|
||||
|
||||
.pdf-reader-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
background: var(--bg-page);
|
||||
}
|
||||
|
||||
.pdf-reader-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.pdf-viewer-container {
|
||||
flex: 1;
|
||||
padding: var(--space-lg);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.action-buttons-container {
|
||||
position: fixed;
|
||||
bottom: var(--space-3xl);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: var(--color-primary);
|
||||
color: var(--text-inverse);
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-md);
|
||||
}
|
||||
|
||||
.action-button:hover:not(:disabled) {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.action-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-button-success {
|
||||
background: var(--color-success);
|
||||
}
|
||||
|
||||
.action-button-success:hover:not(:disabled) {
|
||||
background: var(--color-success-hover);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-xs);
|
||||
margin-top: var(--space-xs);
|
||||
max-width: 300px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-message-danger {
|
||||
background: var(--bg-error);
|
||||
color: var(--color-error);
|
||||
border: 1px solid var(--border-error);
|
||||
}
|
||||
|
||||
.error-message-success {
|
||||
background: var(--bg-success);
|
||||
color: var(--color-success);
|
||||
border: 1px solid var(--border-success);
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.error-message p {
|
||||
margin: var(--space-xs) 0 0 0;
|
||||
font-size: var(--font-xs);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.sidebar-toggle-button {
|
||||
position: fixed;
|
||||
bottom: var(--space-lg);
|
||||
right: var(--space-lg);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: var(--color-primary);
|
||||
color: var(--text-inverse);
|
||||
border: none;
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-md);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.sidebar-toggle-button:hover {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user