| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041 |
- <template>
- <el-dialog
- v-model="visible"
- :title="dialogTitle"
- :width="dialogWidth"
- :fullscreen="isFullscreen"
- :class="getDialogClass()"
- append-to-body
- height="100vh"
- top="0vh"
- :destroy-on-close="true"
- :close-on-click-modal="false"
- :close-on-press-escape="true"
- @close="handleClose"
- >
- <template #header="{ titleId, titleClass }">
- <div class="preview-header">
- <div class="preview-title-section">
- <h4 :id="titleId" :class="titleClass">
- <el-icon class="title-icon">
- <component :is="getFileIcon(fileInfo.type)" />
- </el-icon>
- {{ fileInfo.name }}
- </h4>
- <span class="file-size-info">{{ fileInfo.size }}</span>
- </div>
- <div class="preview-controls">
- <el-tooltip :content="isFullscreen ? '退出全屏' : '全屏显示'" placement="bottom">
- <el-button
- size="small"
- :icon="isFullscreen ? Minus : FullScreen"
- @click="toggleFullscreen"
- circle
- type="primary"
- />
- </el-tooltip>
- <el-tooltip content="刷新预览" placement="bottom">
- <el-button
- size="small"
- :icon="RefreshRight"
- @click="refreshPreview"
- circle
- />
- </el-tooltip>
- </div>
- </div>
- </template>
- <div class="preview-container" v-loading="loading" :element-loading-text="getLoadingText()">
- <!-- PDF预览 -->
- <div
- class="preview-content pdf-content"
- v-if="!loading && !error && pdfBlobUrl"
- v-show="pdfLoaded"
- >
- <div class="pdf-embed-container">
- <transition name="pdf-fade" mode="out-in">
- <div :key="currentPage" class="pdf-page-wrapper">
- <VuePdfEmbed
- :source="pdfBlobUrl"
- :page="currentPage"
- :width="Math.floor(600 * scale)"
- class="pdf-embed"
- @loaded="handlePdfLoaded"
- @loading-failed="handlePdfError"
- @progress="handlePdfProgress"
- @rendered="handlePageRendered"
- />
- </div>
- </transition>
-
- <!-- PDF控制栏 - 集成所有按钮 -->
- <div class="pdf-controls" v-if="totalPages > 0">
- <!-- 翻页控制 -->
- <el-button-group>
- <el-button
- size="small"
- :disabled="currentPage <= 1"
- @click="goToPage(currentPage - 1)"
- :icon="ArrowLeft"
- >
- 上一页
- </el-button>
- <el-input
- v-model.number="pageInput"
- size="small"
- class="page-input"
- @keyup.enter="goToInputPage"
- @blur="goToInputPage"
- />
- <span class="page-separator">/</span>
- <span class="total-pages">{{ totalPages }}</span>
- <el-button
- size="small"
- :disabled="currentPage >= totalPages"
- @click="goToPage(currentPage + 1)"
- :icon="ArrowRight"
- >
- 下一页
- </el-button>
- </el-button-group>
-
- <!-- 缩放控制 -->
- <el-button-group class="zoom-controls">
- <el-button
- size="small"
- @click="zoomOut"
- :disabled="scale <= 0.5"
- :icon="ZoomOut"
- >
- 缩小
- </el-button>
- <span class="zoom-display">{{ Math.round(scale * 100) }}%</span>
- <el-button
- size="small"
- @click="zoomIn"
- :disabled="scale >= 3"
- :icon="ZoomIn"
- >
- 放大
- </el-button>
- <el-button
- size="small"
- @click="resetZoom"
- :icon="RefreshRight"
- >
- 重置
- </el-button>
- </el-button-group>
- <!-- 操作按钮 -->
- <el-button-group class="action-controls">
- <el-button
- size="small"
- :icon="Download"
- @click="downloadFile"
- :disabled="!pdfBlobUrl"
- >
- 下载
- </el-button>
- <el-button
- size="small"
- :icon="Share"
- @click="openPdfInNewWindow"
- :disabled="!pdfBlobUrl"
- >
- 浏览器打开
- </el-button>
- <el-button
- size="small"
- @click="handleClose"
- >
- 关闭
- </el-button>
- </el-button-group>
- </div>
- </div>
- </div>
-
- <!-- 预览错误状态 -->
- <div v-else-if="error || (!loading && !pdfBlobUrl)" class="preview-error">
- <el-icon size="64" color="#f56c6c">
- <WarningFilled />
- </el-icon>
- <p class="error-title">无法预览此PDF文档</p>
- <p class="error-tip">可能的原因:</p>
- <ul class="error-reasons">
- <li>网络连接问题</li>
- <li>文件获取失败</li>
- <li>PDF文件损坏或格式不正确</li>
- <li>服务器返回错误</li>
- <li>PDF文件过大</li>
- <li>PDF文件受密码保护</li>
- </ul>
- <div class="error-actions">
- <el-button type="primary" @click="openPdfInNewWindow">
- 浏览器中打开
- </el-button>
- <el-button @click="refreshPreview">重试</el-button>
- </div>
- </div>
- </div>
- </el-dialog>
- </template>
- <script setup>
- import {
- Download,
- FullScreen,
- Minus,
- WarningFilled,
- RefreshRight,
- Share,
- Document,
- ArrowLeft,
- ArrowRight,
- ZoomIn,
- ZoomOut
- } from '@element-plus/icons-vue'
- import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
- import { ElMessage } from 'element-plus'
- import VuePdfEmbed from 'vue-pdf-embed'
- import 'vue-pdf-embed/dist/styles/annotationLayer.css'
- import 'vue-pdf-embed/dist/styles/textLayer.css'
- import { previewPrepareReport } from '@/api/laboratory/functional/report'
- import { PipeTaskOrderApi } from '@/api/pressure2/pipetaskorder'
- const props = defineProps({
- modelValue: {
- type: Boolean,
- default: false
- },
- id: {
- type: String,
- required: true
- },
- isEditOtherFileFlag: {
- type: Boolean,
- default: false
- },
- filename: {
- type: String,
- default: ''
- },
- title: {
- type: String,
- default: '文件预览'
- }
- })
- const emit = defineEmits(['update:modelValue', 'close'])
- // 响应式数据
- const visible = ref(false)
- const loading = ref(false)
- const error = ref(false)
- const isFullscreen = ref(false)
- const pdfBlobUrl = ref('')
- const actualFileSize = ref(0)
- const pdfLoaded = ref(false) // 新增:标记PDF是否已加载完成
- // PDF 相关状态
- const currentPage = ref(1)
- const totalPages = ref(0)
- const scale = ref(2.0)
- const pageInput = ref(1)
- const isPageChanging = ref(false) // 新增:标记页面切换状态
- // 文件信息
- const fileInfo = computed(() => {
- const filename = props.filename || 'report.pdf'
- const filesize = formatFileSize(actualFileSize.value)
-
- return {
- name: filename,
- size: filesize || '计算中...',
- type: 'application/pdf',
- id: props.id
- }
- })
- // 计算属性
- const dialogTitle = computed(() => {
- return `PDF预览${fileInfo.value.name ? ' - ' + fileInfo.value.name : ''}`
- })
- const dialogWidth = computed(() => {
- if (isFullscreen.value) return '100%'
- return window.innerWidth > 1400 ? '85%' : window.innerWidth > 1200 ? '90%' : '95%'
- })
- // 获取文件图标
- const getFileIcon = (type) => {
- return Document
- }
- // 获取对话框样式类
- const getDialogClass = () => {
- return 'pdf-preview-dialog'
- }
- // 获取加载文本
- const getLoadingText = () => {
- return '正在加载PDF文档...'
- }
- // 工具函数
- const formatFileSize = (size) => {
- if (!size || size === 0) return ''
-
- const bytes = Number(size)
- if (isNaN(bytes) || bytes === 0) return ''
-
- const k = 1024
- const sizes = ['B', 'KB', 'MB', 'GB']
- const i = Math.floor(Math.log(bytes) / Math.log(k))
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
- }
- // 优化后的加载预览函数
- const loadPreview = async () => {
- if (!props.id) {
- error.value = true
- return
- }
-
- // 只在真正需要重新加载时才清理blob URL
- const shouldReload = !pdfBlobUrl.value
-
- if (shouldReload && pdfBlobUrl.value) {
- URL.revokeObjectURL(pdfBlobUrl.value)
- pdfBlobUrl.value = ''
- }
-
- // 重置状态
- error.value = false
- if (shouldReload) {
- currentPage.value = 1
- totalPages.value = 0
- pageInput.value = 1
- scale.value = 2.0
- actualFileSize.value = 0
- pdfLoaded.value = false
- }
- loading.value = true
-
- try {
- console.log('开始获取PDF文件流...')
-
- const data = {
- id: props.id,
- // type: '1'
- }
- // if (props.isEditOtherFileFlag == false) {
- // delete data.type
- // }
- const response = await PipeTaskOrderApi.getIssueReportUseLogoPreviewApi(data, {
- responseType: 'blob',
- headers: {
- 'Accept': 'application/pdf, application/octet-stream, */*'
- }
- })
-
- console.log('获取到文件流响应:', response)
-
- let blob
-
- if (response instanceof Blob) {
- blob = response
- if (!blob.type.includes('pdf') && !blob.type.includes('octet-stream') && blob.type !== '') {
- if (blob.type.includes('text') || blob.type.includes('xml') || blob.type.includes('html')) {
- const text = await blob.text()
- console.error('API返回非PDF数据:', text)
- throw new Error('服务器返回的不是PDF文件,可能是错误信息')
- }
- }
- if (!blob.type.includes('pdf')) {
- blob = new Blob([blob], { type: 'application/pdf' })
- }
- } else if (response instanceof ArrayBuffer) {
- blob = new Blob([response], { type: 'application/pdf' })
- } else if (response.data) {
- if (response.data instanceof Blob) {
- blob = response.data
- if (!blob.type.includes('pdf')) {
- blob = new Blob([blob], { type: 'application/pdf' })
- }
- } else if (response.data instanceof ArrayBuffer) {
- blob = new Blob([response.data], { type: 'application/pdf' })
- } else {
- if (typeof response.data === 'string') {
- console.error('API返回字符串数据:', response.data)
- throw new Error('服务器返回的不是PDF文件数据')
- }
- blob = new Blob([response.data], { type: 'application/pdf' })
- }
- }
- else {
- if (typeof response === 'string') {
- console.error('API返回字符串数据:', response)
- throw new Error('服务器返回的不是PDF文件数据')
- }
- blob = new Blob([response], { type: 'application/pdf' })
- }
-
- console.log('创建的Blob:', blob, 'size:', blob.size, 'type:', blob.type)
-
- if (!blob || blob.size === 0) {
- throw new Error('获取到的文件为空')
- }
-
- await validatePdfBlob(blob)
-
- actualFileSize.value = blob.size
- console.log('自动获取的文件大小:', formatFileSize(blob.size))
-
- const blobUrl = URL.createObjectURL(blob)
- pdfBlobUrl.value = blobUrl
-
- console.log('创建的Blob URL:', blobUrl)
-
- await nextTick()
-
- } catch (error) {
- console.error('PDF加载失败:', error)
- handlePdfError(error)
- ElMessage.error('PDF文件加载失败: ' + (error.message || '未知错误'))
- } finally {
- loading.value = false
- }
- }
- const validatePdfBlob = async (blob) => {
- return new Promise((resolve, reject) => {
- const reader = new FileReader()
-
- reader.onload = function(e) {
- const arrayBuffer = e.target.result
- const uint8Array = new Uint8Array(arrayBuffer.slice(0, 10))
-
- const pdfHeader = [0x25, 0x50, 0x44, 0x46, 0x2D]
- const headerMatch = pdfHeader.every((byte, index) => uint8Array[index] === byte)
-
- if (!headerMatch) {
- const text = new TextDecoder().decode(uint8Array)
- if (text.includes('<') || text.includes('<?xml')) {
- reject(new Error('服务器返回的是XML/HTML错误响应,不是PDF文件'))
- return
- }
- reject(new Error('文件格式不正确,不是有效的PDF文件'))
- return
- }
-
- resolve()
- }
-
- reader.onerror = function() {
- reject(new Error('文件读取失败'))
- }
-
- reader.readAsArrayBuffer(blob.slice(0, 1024))
- })
- }
- // PDF事件处理
- const handlePdfLoaded = (pdf) => {
- console.log('PDF加载成功:', pdf)
- loading.value = false
- error.value = false
- totalPages.value = pdf.numPages
- pdfLoaded.value = true
-
- if (currentPage.value === 0 || currentPage.value > pdf.numPages) {
- currentPage.value = 1
- pageInput.value = 1
- }
-
- // ElMessage.success('PDF加载成功')
- }
- // 新增:处理页面渲染完成
- const handlePageRendered = () => {
- isPageChanging.value = false
- }
- const handlePdfError = (errorInfo) => {
- console.error('PDF渲染错误:', errorInfo)
- loading.value = false
- error.value = true
- pdfLoaded.value = false
- isPageChanging.value = false
- ElMessage.error('PDF渲染失败,请尝试刷新或在浏览器中直接打开')
- }
- const handlePdfProgress = (progressParams) => {
- console.log('PDF加载进度:', progressParams)
- }
- // 优化页面导航 - 防止闪烁
- const goToPage = (page) => {
- if (page >= 1 && page <= totalPages.value && !isPageChanging.value) {
- isPageChanging.value = true
- currentPage.value = page
- pageInput.value = page
-
- // 给一个小延迟让过渡动画更平滑
- setTimeout(() => {
- if (isPageChanging.value) {
- isPageChanging.value = false
- }
- }, 300)
- }
- }
- const goToInputPage = () => {
- const page = parseInt(pageInput.value)
- if (isNaN(page) || page < 1 || page > totalPages.value) {
- pageInput.value = currentPage.value
- ElMessage.warning('请输入有效的页码')
- return
- }
- goToPage(page)
- }
- // 其他事件处理函数
- const refreshPreview = () => {
- pdfLoaded.value = false
- loadPreview()
- }
- const toggleFullscreen = () => {
- isFullscreen.value = !isFullscreen.value
- }
- const openPdfInNewWindow = () => {
- if (!pdfBlobUrl.value) return
- try {
- window.open(pdfBlobUrl.value, '_blank', 'noopener,noreferrer')
- ElMessage.success('已在新窗口打开PDF文件')
- } catch (error) {
- ElMessage.error('新窗口打开失败,请检查浏览器设置')
- }
- }
- const downloadFile = () => {
- if (!pdfBlobUrl.value) {
- ElMessage.warning('文件尚未加载完成')
- return
- }
-
- try {
- const link = document.createElement('a')
- link.href = pdfBlobUrl.value
-
- let filename = fileInfo.value.name
- if (!filename.toLowerCase().endsWith('.pdf')) {
- filename += '.pdf'
- }
-
- link.download = filename
- link.target = '_blank'
- link.rel = 'noopener noreferrer'
-
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
-
- ElMessage.success('文件下载已开始')
- } catch (error) {
- console.error('下载失败:', error)
- ElMessage.error('下载失败,请重试')
- }
- }
- // 缩放控制
- const zoomIn = () => {
- if (scale.value < 3) {
- scale.value = Math.min(Math.round((scale.value * 1.2) * 10) / 10, 3)
- }
- }
- const zoomOut = () => {
- if (scale.value > 0.5) {
- scale.value = Math.max(Math.round((scale.value / 1.2) * 10) / 10, 0.5)
- }
- }
- const resetZoom = () => {
- scale.value = 2.0
- }
- // 键盘快捷键支持
- const handleKeydown = (event) => {
- if (!visible.value) return
-
- if (event.ctrlKey || event.metaKey) {
- if (event.key === '=' || event.key === '+') {
- event.preventDefault()
- zoomIn()
- } else if (event.key === '-') {
- event.preventDefault()
- zoomOut()
- } else if (event.key === '0') {
- event.preventDefault()
- resetZoom()
- }
- }
-
- // 支持方向键翻页
- if (event.key === 'ArrowLeft' && currentPage.value > 1) {
- event.preventDefault()
- goToPage(currentPage.value - 1)
- } else if (event.key === 'ArrowRight' && currentPage.value < totalPages.value) {
- event.preventDefault()
- goToPage(currentPage.value + 1)
- }
- }
- const handleClose = () => {
- visible.value = false
- emit('update:modelValue', false)
- emit('close')
-
- // 清理时保留blob URL,因为对话框可能会再次打开
- // 只在组件销毁时清理
- }
- // 监听器
- watch(
- () => props.modelValue,
- (newVal) => {
- visible.value = newVal
- if (newVal && !pdfBlobUrl.value) {
- loadPreview()
- }
- },
- { immediate: true }
- )
- onMounted(() => {
- document.addEventListener('keydown', handleKeydown)
- })
- // 清理
- onUnmounted(() => {
- document.removeEventListener('keydown', handleKeydown)
- if (pdfBlobUrl.value) {
- URL.revokeObjectURL(pdfBlobUrl.value)
- }
- })
- </script>
- <style scoped lang="scss">
- $a4-width: 210mm;
- $a4-height: 297mm;
- $a4-ratio: 1.414;
- $page-shadow: 0 4px 12px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
- $page-shadow-hover: 0 8px 25px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.1);
- .pdf-wrapper {
- transition: transform 0.3s ease;
- display: flex;
- justify-content: center;
- padding: 20px 0;
- }
- .pdf-embed {
- border-radius: 4px;
- box-shadow: $page-shadow;
- background: #fff;
- transition: all 0.3s ease;
-
- &:hover {
- box-shadow: $page-shadow-hover;
- }
- }
- .preview-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0;
- background: #f8f9fa;
- border-radius: 8px 8px 0 0;
- margin: -20px -20px 20px -20px;
- padding: 16px 20px;
- border-bottom: 1px solid #e4e7ed;
- }
- .preview-title-section {
- display: flex;
- align-items: center;
- gap: 12px;
- flex: 1;
- }
- .preview-title-section h4 {
- margin: 0;
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 16px;
- font-weight: 600;
- color: #303133;
- }
- .title-icon {
- color: #e74c3c;
- font-size: 18px;
- }
- .file-size-info {
- color: #909399;
- font-size: 12px;
- background: rgba(64, 158, 255, 0.1);
- color: #409eff;
- padding: 4px 8px;
- border-radius: 12px;
- font-weight: 500;
- }
- .preview-controls {
- display: flex;
- gap: 8px;
- }
- .preview-container {
- position: relative;
- min-height: 600px;
- background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
- border-radius: 12px;
- padding: 20px;
- margin: 0 -20px;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- .preview-content {
- width: 100%;
- height: 100%;
- display: flex;
- justify-content: center;
- align-items: flex-start;
- }
- .pdf-content {
- flex-direction: column;
- align-items: center;
- gap: 20px;
- width: 100%;
- max-width: 900px;
- }
- .pdf-embed-container {
- width: 100%;
- display: flex;
- flex-direction: column;
- gap: 20px;
- align-items: center;
- }
- :deep(.vue-pdf-embed) {
- background: transparent !important;
-
- .vue-pdf-embed__page {
- margin: 0 auto 20px;
- background: #ffffff;
- box-shadow: $page-shadow;
- border-radius: 8px;
- padding: 0;
- transition: all 0.3s ease;
- position: relative;
- overflow: hidden;
-
- &::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: linear-gradient(
- 45deg,
- transparent 49%,
- rgba(255, 255, 255, 0.03) 49%,
- rgba(255, 255, 255, 0.03) 51%,
- transparent 51%
- );
- pointer-events: none;
- z-index: 1;
- }
-
- &:hover {
- box-shadow: $page-shadow-hover;
- transform: translateY(-2px);
- }
-
- canvas {
- display: block;
- border-radius: 8px;
- position: relative;
- z-index: 0;
- }
- }
-
- .vue-pdf-embed__container {
- background: transparent !important;
- }
- }
- .pdf-controls {
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex-wrap: wrap;
- gap: 12px;
- padding: 16px 20px;
- background: rgba(255, 255, 255, 0.95);
- backdrop-filter: blur(10px);
- border-radius: 12px;
- border: 1px solid rgba(255, 255, 255, 0.2);
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
- position: sticky;
- bottom: 20px;
- z-index: 100;
- margin: 0 auto;
- width: 100%;
- max-width: 900px;
- }
- .page-input {
- width: 60px;
- text-align: center;
-
- :deep(.el-input__inner) {
- text-align: center;
- border-radius: 6px;
- font-weight: 500;
- }
- }
- .page-separator {
- margin: 0 8px;
- color: #909399;
- font-weight: 500;
- }
- .total-pages {
- color: #606266;
- font-weight: 600;
- }
- .zoom-controls {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .action-controls {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .zoom-display {
- min-width: 55px;
- text-align: center;
- font-size: 14px;
- color: #606266;
- font-weight: 600;
- background: rgba(64, 158, 255, 0.1);
- padding: 4px 8px;
- border-radius: 6px;
- color: #409eff;
- }
- .preview-error {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 60px 20px;
- text-align: center;
- color: #606266;
- height: 100%;
- min-height: 400px;
- background: rgba(255, 255, 255, 0.8);
- border-radius: 12px;
- backdrop-filter: blur(10px);
- }
- .error-title {
- font-size: 20px;
- font-weight: 600;
- margin: 20px 0 12px;
- color: #f56c6c;
- }
- .error-tip {
- margin: 16px 0 12px;
- font-size: 15px;
- color: #909399;
- font-weight: 500;
- }
- .error-reasons {
- text-align: left;
- margin: 16px 0 24px;
- padding: 16px 20px;
- background: rgba(245, 108, 108, 0.05);
- border-radius: 8px;
- border-left: 4px solid #f56c6c;
-
- li {
- margin: 6px 0;
- font-size: 14px;
- color: #606266;
- line-height: 1.5;
- }
- }
- .error-actions {
- display: flex;
- gap: 12px;
- margin-top: 24px;
- }
- :deep(.el-loading-mask) {
- background-color: rgba(248, 249, 250, 0.95);
- backdrop-filter: blur(5px);
- border-radius: 12px;
-
- .el-loading-spinner {
- .el-loading-text {
- color: #606266;
- font-weight: 500;
- margin-top: 16px;
- }
-
- .circular {
- width: 50px;
- height: 50px;
- color: #409eff;
- }
- }
- }
- @media (max-width: 1200px) {
- .preview-container {
- padding: 15px;
- }
-
- .pdf-controls {
- max-width: 100%;
- margin: 0 15px;
- }
- }
- @media (max-width: 768px) {
- .preview-header {
- flex-direction: column;
- gap: 12px;
- align-items: stretch;
- padding: 12px 16px;
- }
-
- .preview-controls {
- justify-content: center;
- }
-
- .pdf-controls {
- flex-direction: column;
- gap: 16px;
- padding: 16px;
- position: relative;
- bottom: auto;
- margin: 20px 0 0;
-
- .el-button-group {
- width: 100%;
- justify-content: center;
- }
- }
-
- .preview-container {
- padding: 10px;
- margin: 0 -10px;
- }
- }
- @media (max-width: 480px) {
- .pdf-controls {
- .el-button-group {
- flex-wrap: wrap;
- gap: 8px;
- }
-
- .zoom-controls,
- .action-controls {
- flex-wrap: wrap;
- justify-content: center;
- }
- }
-
- .preview-header {
- padding: 10px 12px;
- }
-
- .preview-title-section h4 {
- font-size: 14px;
- }
- }
- :deep(.pdf-preview-dialog) {
- .el-dialog__body {
- background: #ffffff;
- }
-
- .el-dialog__header {
- padding: 0;
- margin: 0;
- border-bottom: none;
- }
-
- .el-dialog__headerbtn {
- top: 16px;
- right: 16px;
- width: 32px;
- height: 32px;
- background: rgba(255, 255, 255, 0.9);
- border-radius: 50%;
- transition: all 0.3s ease;
-
- &:hover {
- background: rgba(245, 108, 108, 0.1);
-
- .el-dialog__close {
- color: #f56c6c;
- }
- }
- }
-
- .el-dialog__footer {
- padding: 0;
- border-top: none;
- }
- }
- :deep(.is-fullscreen) {
- .preview-container {
- min-height: calc(100vh - 200px);
- }
-
- .pdf-controls {
- position: fixed;
- bottom: 30px;
- left: 50%;
- transform: translateX(-50%);
- z-index: 1000;
- }
- }
- </style>
|