668 lines
19 KiB
Vue
668 lines
19 KiB
Vue
<script setup lang="ts">
|
||
import {
|
||
GlobalWorkerOptions,
|
||
TextLayer,
|
||
getDocument,
|
||
type PDFDocumentLoadingTask,
|
||
type PDFDocumentProxy,
|
||
type RenderTask,
|
||
} from 'pdfjs-dist'
|
||
import pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'
|
||
import { computed, nextTick, onBeforeUnmount, ref, shallowRef, watch } from 'vue'
|
||
import {
|
||
getDataProcessPdfPages,
|
||
getDataProcessSourceRawUrl,
|
||
type DataProcessPdfPageRange,
|
||
} from '@/api/modules/dataProcess'
|
||
import type { PreviewItem } from './types'
|
||
|
||
GlobalWorkerOptions.workerSrc = pdfWorkerUrl
|
||
|
||
const props = defineProps<{
|
||
taskId: string | number | null
|
||
sourceFileId: string | number | null
|
||
fileName: string
|
||
selectedItem: PreviewItem | null
|
||
}>()
|
||
|
||
const scrollRef = ref<HTMLElement | null>(null)
|
||
const pageRef = ref<HTMLElement | null>(null)
|
||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||
const textLayerRef = ref<HTMLElement | null>(null)
|
||
const documentRef = shallowRef<PDFDocumentProxy | null>(null)
|
||
const pageRanges = ref<DataProcessPdfPageRange[]>([])
|
||
const currentPage = ref(1)
|
||
const pageCount = ref(0)
|
||
const zoom = ref(1)
|
||
const renderedScale = ref(1)
|
||
const loading = ref(false)
|
||
const rendering = ref(false)
|
||
const errorMessage = ref('')
|
||
const highlightState = ref<'idle' | 'highlighted' | 'unmatched' | 'manual'>('idle')
|
||
|
||
let loadingTask: PDFDocumentLoadingTask | null = null
|
||
let renderTask: RenderTask | null = null
|
||
let textLayer: TextLayer | null = null
|
||
let loadSequence = 0
|
||
let renderSequence = 0
|
||
let renderedPageNumber = 0
|
||
let resizeFrame = 0
|
||
|
||
const sourceUrl = computed(() => (
|
||
props.taskId != null && props.sourceFileId != null
|
||
? getDataProcessSourceRawUrl(props.taskId, props.sourceFileId)
|
||
: ''
|
||
))
|
||
|
||
const pageStyle = computed(() => ({
|
||
'--total-scale-factor': String(renderedScale.value),
|
||
}))
|
||
|
||
const locationText = computed(() => {
|
||
if (highlightState.value === 'highlighted') return `切片原文已在第 ${currentPage.value} 页高亮`
|
||
if (highlightState.value === 'unmatched') return `已定位第 ${currentPage.value} 页,未匹配到可高亮文字`
|
||
if (highlightState.value === 'manual') return '手动新增切片没有原文位置'
|
||
return pageCount.value ? `第 ${currentPage.value} / ${pageCount.value} 页` : '正在读取 PDF'
|
||
})
|
||
|
||
function pageForItem(item: PreviewItem | null) {
|
||
if (!item) return null
|
||
if (item.sourceStart == null || item.sourceEnd == null) {
|
||
const pageNumber = item.sourcePages?.[0]
|
||
return pageNumber == null
|
||
? null
|
||
: pageRanges.value.find((page) => page.page_number === pageNumber) ?? null
|
||
}
|
||
return pageRanges.value.find((page) => (
|
||
item.sourceStart! >= page.source_start && item.sourceStart! < page.source_end
|
||
)) ?? pageRanges.value.find((page) => (
|
||
item.sourceStart! < page.source_end && item.sourceEnd! > page.source_start
|
||
)) ?? null
|
||
}
|
||
|
||
function selectedTextForPage(page: DataProcessPdfPageRange) {
|
||
const item = props.selectedItem
|
||
if (!item) return ''
|
||
if (item.sourceStart == null || item.sourceEnd == null) {
|
||
return item.sourcePages?.includes(page.page_number) ? item.originalContent : ''
|
||
}
|
||
const intersectionStart = Math.max(item.sourceStart, page.source_start)
|
||
const intersectionEnd = Math.min(item.sourceEnd, page.source_end)
|
||
if (intersectionEnd <= intersectionStart) return ''
|
||
const relativeStart = Math.max(0, intersectionStart - item.sourceStart)
|
||
const relativeEnd = Math.max(relativeStart, intersectionEnd - item.sourceStart)
|
||
return item.originalContent.slice(relativeStart, relativeEnd)
|
||
}
|
||
|
||
function normalizeLocatorText(value: string) {
|
||
return value.normalize('NFKC').replace(/[^\p{L}\p{N}]+/gu, '').toLocaleLowerCase()
|
||
}
|
||
|
||
function nearestOccurrence(haystack: string, needle: string, expectedIndex: number) {
|
||
let nearest = -1
|
||
let nearestDistance = Number.POSITIVE_INFINITY
|
||
let cursor = haystack.indexOf(needle)
|
||
while (cursor >= 0) {
|
||
const distance = Math.abs(cursor - expectedIndex)
|
||
if (distance < nearestDistance) {
|
||
nearest = cursor
|
||
nearestDistance = distance
|
||
}
|
||
cursor = haystack.indexOf(needle, cursor + 1)
|
||
}
|
||
return nearest
|
||
}
|
||
|
||
function findHighlight(
|
||
itemStrings: string[],
|
||
selectedText: string,
|
||
page: DataProcessPdfPageRange,
|
||
) {
|
||
const itemRanges: Array<{ start: number; end: number }> = []
|
||
let pageText = ''
|
||
for (const item of itemStrings) {
|
||
const start = pageText.length
|
||
pageText += normalizeLocatorText(item)
|
||
itemRanges.push({ start, end: pageText.length })
|
||
}
|
||
|
||
const target = normalizeLocatorText(selectedText)
|
||
if (!pageText || !target) return []
|
||
const itemStart = props.selectedItem?.sourceStart ?? page.source_start
|
||
const sourceLength = Math.max(1, page.source_end - page.source_start)
|
||
const expectedRatio = Math.min(1, Math.max(0, (itemStart - page.source_start) / sourceLength))
|
||
const expectedIndex = Math.round(pageText.length * expectedRatio)
|
||
const anchorLengths = [target.length, 120, 80, 48, 24, 12, 8]
|
||
.map((length) => Math.min(length, target.length))
|
||
.filter((length, index, values) => length >= 4 && values.indexOf(length) === index)
|
||
|
||
for (const anchorLength of anchorLengths) {
|
||
const anchor = target.slice(0, anchorLength)
|
||
const matchStart = nearestOccurrence(pageText, anchor, expectedIndex)
|
||
if (matchStart < 0) continue
|
||
const matchEnd = matchStart + anchor.length
|
||
return itemRanges.reduce<number[]>((matches, range, index) => {
|
||
if (range.end > matchStart && range.start < matchEnd) matches.push(index)
|
||
return matches
|
||
}, [])
|
||
}
|
||
return []
|
||
}
|
||
|
||
function applySelectedHighlight(page: DataProcessPdfPageRange | null) {
|
||
const item = props.selectedItem
|
||
for (const element of textLayer?.textDivs ?? []) {
|
||
element.classList.remove('is-slice-highlighted')
|
||
}
|
||
if (!item || (
|
||
(item.sourceStart == null || item.sourceEnd == null)
|
||
&& !item.sourcePages?.length
|
||
)) {
|
||
highlightState.value = item ? 'manual' : 'idle'
|
||
return
|
||
}
|
||
if (!page || page.page_number !== currentPage.value || !textLayer) {
|
||
highlightState.value = 'idle'
|
||
return
|
||
}
|
||
const matches = findHighlight(
|
||
textLayer.textContentItemsStr,
|
||
selectedTextForPage(page),
|
||
page,
|
||
)
|
||
for (const index of matches) {
|
||
textLayer.textDivs[index]?.classList.add('is-slice-highlighted')
|
||
}
|
||
highlightState.value = matches.length ? 'highlighted' : 'unmatched'
|
||
}
|
||
|
||
async function renderCurrentPage(force = false) {
|
||
const document = documentRef.value
|
||
const pageContainer = pageRef.value
|
||
const canvas = canvasRef.value
|
||
const layerContainer = textLayerRef.value
|
||
const scroller = scrollRef.value
|
||
if (!document || !pageContainer || !canvas || !layerContainer || !scroller) return
|
||
|
||
const selectedPage = pageForItem(props.selectedItem)
|
||
if (!force && renderedPageNumber === currentPage.value && textLayer) {
|
||
applySelectedHighlight(selectedPage)
|
||
return
|
||
}
|
||
|
||
const sequence = ++renderSequence
|
||
renderTask?.cancel()
|
||
textLayer?.cancel()
|
||
renderTask = null
|
||
textLayer = null
|
||
rendering.value = true
|
||
errorMessage.value = ''
|
||
|
||
try {
|
||
const page = await document.getPage(currentPage.value)
|
||
if (sequence !== renderSequence) return
|
||
const baseViewport = page.getViewport({ scale: 1 })
|
||
const availableWidth = Math.max(280, scroller.clientWidth - 36)
|
||
const scale = (availableWidth / baseViewport.width) * zoom.value
|
||
const viewport = page.getViewport({ scale })
|
||
const outputScale = Math.max(1, window.devicePixelRatio || 1)
|
||
const context = canvas.getContext('2d')
|
||
if (!context) throw new Error('浏览器无法创建 PDF 画布')
|
||
|
||
renderedScale.value = scale
|
||
pageContainer.style.width = `${viewport.width}px`
|
||
pageContainer.style.height = `${viewport.height}px`
|
||
canvas.width = Math.floor(viewport.width * outputScale)
|
||
canvas.height = Math.floor(viewport.height * outputScale)
|
||
canvas.style.width = `${viewport.width}px`
|
||
canvas.style.height = `${viewport.height}px`
|
||
layerContainer.replaceChildren()
|
||
|
||
const activeRenderTask = page.render({
|
||
canvas,
|
||
viewport,
|
||
transform: outputScale === 1 ? undefined : [outputScale, 0, 0, outputScale, 0, 0],
|
||
})
|
||
renderTask = activeRenderTask
|
||
// 立即挂接取消处理,避免 ResizeObserver 触发重绘时产生未处理的取消异常。
|
||
const canvasRenderPromise = activeRenderTask.promise.catch((error: unknown) => {
|
||
if (
|
||
sequence !== renderSequence
|
||
|| (error instanceof Error && error.name === 'RenderingCancelledException')
|
||
) return
|
||
throw error
|
||
})
|
||
// WebKit 兼容:PDF.js 的 getTextContent() 依赖 ReadableStream 异步迭代,
|
||
// 部分 Safari/WKWebView 未实现该接口。TextLayer 可直接通过 getReader() 消费文本流。
|
||
const textContentSource = page.streamTextContent()
|
||
if (sequence !== renderSequence) return
|
||
const activeTextLayer = new TextLayer({
|
||
textContentSource,
|
||
container: layerContainer,
|
||
viewport,
|
||
})
|
||
textLayer = activeTextLayer
|
||
await Promise.all([canvasRenderPromise, activeTextLayer.render()])
|
||
if (sequence !== renderSequence) return
|
||
renderedPageNumber = currentPage.value
|
||
applySelectedHighlight(selectedPage)
|
||
await nextTick()
|
||
pageContainer.querySelector<HTMLElement>('.is-slice-highlighted')
|
||
?.scrollIntoView({ block: 'center', inline: 'center', behavior: 'smooth' })
|
||
} catch (error) {
|
||
if (sequence !== renderSequence) return
|
||
errorMessage.value = error instanceof Error ? error.message : 'PDF 页面渲染失败'
|
||
} finally {
|
||
if (sequence === renderSequence) rendering.value = false
|
||
}
|
||
}
|
||
|
||
async function locateSelectedItem() {
|
||
if (!documentRef.value) return
|
||
const item = props.selectedItem
|
||
if (!item || (
|
||
(item.sourceStart == null || item.sourceEnd == null)
|
||
&& !item.sourcePages?.length
|
||
)) {
|
||
applySelectedHighlight(null)
|
||
return
|
||
}
|
||
const page = pageForItem(item)
|
||
if (!page) {
|
||
highlightState.value = 'unmatched'
|
||
return
|
||
}
|
||
if (currentPage.value !== page.page_number) {
|
||
currentPage.value = page.page_number
|
||
await renderCurrentPage(true)
|
||
return
|
||
}
|
||
await renderCurrentPage(false)
|
||
}
|
||
|
||
async function loadPdf() {
|
||
const sequence = ++loadSequence
|
||
++renderSequence
|
||
renderedPageNumber = 0
|
||
renderTask?.cancel()
|
||
textLayer?.cancel()
|
||
await loadingTask?.destroy()
|
||
loadingTask = null
|
||
documentRef.value = null
|
||
pageRanges.value = []
|
||
pageCount.value = 0
|
||
currentPage.value = 1
|
||
errorMessage.value = ''
|
||
highlightState.value = 'idle'
|
||
if (!sourceUrl.value || props.taskId == null || props.sourceFileId == null) return
|
||
|
||
loading.value = true
|
||
try {
|
||
const task = getDocument({ url: sourceUrl.value })
|
||
loadingTask = task
|
||
const [mapping, document] = await Promise.all([
|
||
getDataProcessPdfPages(props.taskId, props.sourceFileId),
|
||
task.promise,
|
||
])
|
||
if (sequence !== loadSequence) {
|
||
await task.destroy()
|
||
return
|
||
}
|
||
documentRef.value = document
|
||
pageRanges.value = mapping.pages
|
||
pageCount.value = document.numPages
|
||
await nextTick()
|
||
await locateSelectedItem()
|
||
if (!props.selectedItem) await renderCurrentPage(true)
|
||
} catch (error) {
|
||
if (sequence !== loadSequence) return
|
||
errorMessage.value = error instanceof Error ? error.message : 'PDF 原文件加载失败'
|
||
} finally {
|
||
if (sequence === loadSequence) loading.value = false
|
||
}
|
||
}
|
||
|
||
async function changePage(delta: number) {
|
||
const nextPage = Math.min(pageCount.value, Math.max(1, currentPage.value + delta))
|
||
if (nextPage === currentPage.value) return
|
||
currentPage.value = nextPage
|
||
await renderCurrentPage(true)
|
||
}
|
||
|
||
async function changeZoom(delta: number) {
|
||
const nextZoom = Math.min(2, Math.max(0.6, Number((zoom.value + delta).toFixed(1))))
|
||
if (nextZoom === zoom.value) return
|
||
zoom.value = nextZoom
|
||
await renderCurrentPage(true)
|
||
}
|
||
|
||
function scheduleResizeRender() {
|
||
window.cancelAnimationFrame(resizeFrame)
|
||
resizeFrame = window.requestAnimationFrame(() => {
|
||
if (documentRef.value) void renderCurrentPage(true)
|
||
})
|
||
}
|
||
|
||
const resizeObserver = new ResizeObserver(scheduleResizeRender)
|
||
watch(scrollRef, (element, previous) => {
|
||
if (previous) resizeObserver.unobserve(previous)
|
||
if (element) resizeObserver.observe(element)
|
||
})
|
||
watch(sourceUrl, () => void loadPdf(), { immediate: true })
|
||
watch(() => props.selectedItem?.id, () => void locateSelectedItem())
|
||
|
||
onBeforeUnmount(() => {
|
||
++loadSequence
|
||
++renderSequence
|
||
window.cancelAnimationFrame(resizeFrame)
|
||
resizeObserver.disconnect()
|
||
renderTask?.cancel()
|
||
textLayer?.cancel()
|
||
void loadingTask?.destroy()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="pdf-source-viewer" aria-label="PDF 原文件预览">
|
||
<div v-if="sourceUrl" class="pdf-toolbar">
|
||
<div class="toolbar-group">
|
||
<button type="button" aria-label="上一页" :disabled="currentPage <= 1 || loading" @click="changePage(-1)">
|
||
<i class="fa fa-chevron-left" aria-hidden="true" />
|
||
</button>
|
||
<span class="page-indicator">{{ currentPage }} / {{ pageCount || '—' }}</span>
|
||
<button type="button" aria-label="下一页" :disabled="currentPage >= pageCount || loading" @click="changePage(1)">
|
||
<i class="fa fa-chevron-right" aria-hidden="true" />
|
||
</button>
|
||
</div>
|
||
<span class="location-state" :class="`is-${highlightState}`" role="status">
|
||
<i v-if="highlightState === 'highlighted'" class="fa fa-map-marker" aria-hidden="true" />
|
||
{{ locationText }}
|
||
</span>
|
||
<div class="toolbar-group">
|
||
<button type="button" aria-label="缩小" :disabled="zoom <= 0.6 || loading" @click="changeZoom(-0.1)">−</button>
|
||
<span class="zoom-indicator">{{ Math.round(zoom * 100) }}%</span>
|
||
<button type="button" aria-label="放大" :disabled="zoom >= 2 || loading" @click="changeZoom(0.1)">+</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="sourceUrl" ref="scrollRef" class="pdf-scroll" tabindex="0" :aria-label="`PDF 预览:${fileName}`">
|
||
<div
|
||
ref="pageRef"
|
||
class="pdf-page"
|
||
:style="pageStyle"
|
||
:data-page-number="currentPage"
|
||
>
|
||
<canvas ref="canvasRef" class="pdf-canvas" />
|
||
<div ref="textLayerRef" class="pdf-text-layer" />
|
||
</div>
|
||
<div v-if="loading || rendering" class="pdf-loading" role="status">
|
||
<i class="fa fa-spinner fa-spin" aria-hidden="true" />
|
||
{{ loading ? '正在加载 PDF…' : '正在渲染页面…' }}
|
||
</div>
|
||
<div v-if="errorMessage" class="pdf-error" role="alert">
|
||
<i class="fa fa-exclamation-circle" aria-hidden="true" />
|
||
<strong>PDF 预览失败</strong>
|
||
<span>{{ errorMessage }}</span>
|
||
<a :href="sourceUrl" target="_blank" rel="noopener noreferrer">在新窗口打开原件</a>
|
||
</div>
|
||
</div>
|
||
<div v-else class="pdf-unavailable" role="status">
|
||
<i class="fa fa-file-pdf-o" aria-hidden="true" />
|
||
<strong>PDF 原文件暂不可预览</strong>
|
||
<span>请重新上传该文件后再试。</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.pdf-source-viewer {
|
||
display: flex;
|
||
min-height: 0;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
background: #525659;
|
||
}
|
||
|
||
.pdf-toolbar {
|
||
display: grid;
|
||
min-height: 44px;
|
||
flex: 0 0 auto;
|
||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 0 12px;
|
||
color: #f2f4f7;
|
||
background: #323639;
|
||
border-bottom: 1px solid #1f2427;
|
||
}
|
||
|
||
.toolbar-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
|
||
button {
|
||
display: inline-flex;
|
||
width: 30px;
|
||
height: 30px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #f2f4f7;
|
||
font: inherit;
|
||
font-size: 16px;
|
||
background: transparent;
|
||
border: 0;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
|
||
&:hover:not(:disabled) {
|
||
background: rgb(255 255 255 / 12%);
|
||
}
|
||
|
||
&:disabled {
|
||
color: #7d8387;
|
||
cursor: not-allowed;
|
||
}
|
||
}
|
||
}
|
||
|
||
.page-indicator,
|
||
.zoom-indicator {
|
||
min-width: 54px;
|
||
color: #e4e7ec;
|
||
font-size: 12px;
|
||
text-align: center;
|
||
}
|
||
|
||
.location-state {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
color: #d0d5dd;
|
||
font-size: 12px;
|
||
text-align: center;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
|
||
&.is-highlighted {
|
||
color: #ffd666;
|
||
}
|
||
|
||
&.is-unmatched {
|
||
color: #fdb022;
|
||
}
|
||
}
|
||
|
||
.pdf-scroll {
|
||
position: relative;
|
||
display: flex;
|
||
min-height: 538px;
|
||
flex: 1;
|
||
align-items: flex-start;
|
||
justify-content: center;
|
||
overflow: auto;
|
||
padding: 18px;
|
||
outline: none;
|
||
}
|
||
|
||
.pdf-page {
|
||
position: relative;
|
||
flex: 0 0 auto;
|
||
overflow: hidden;
|
||
background: #fff;
|
||
box-shadow: 0 2px 12px rgb(0 0 0 / 34%);
|
||
}
|
||
|
||
.pdf-canvas {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: block;
|
||
}
|
||
|
||
.pdf-text-layer {
|
||
--min-font-size: 1;
|
||
--text-scale-factor: calc(var(--total-scale-factor) * var(--min-font-size));
|
||
--min-font-size-inv: calc(1 / var(--min-font-size));
|
||
|
||
position: absolute;
|
||
z-index: 1;
|
||
inset: 0;
|
||
overflow: clip;
|
||
color-scheme: only light;
|
||
line-height: 1;
|
||
letter-spacing: normal;
|
||
word-spacing: normal;
|
||
text-align: initial;
|
||
text-size-adjust: none;
|
||
forced-color-adjust: none;
|
||
transform-origin: 0 0;
|
||
caret-color: CanvasText;
|
||
}
|
||
|
||
.pdf-text-layer :deep(span),
|
||
.pdf-text-layer :deep(br) {
|
||
position: absolute;
|
||
color: transparent;
|
||
white-space: pre;
|
||
cursor: text;
|
||
user-select: text;
|
||
transform-origin: 0 0;
|
||
}
|
||
|
||
.pdf-text-layer > :deep(:not(.markedContent)),
|
||
.pdf-text-layer :deep(.markedContent span:not(.markedContent)) {
|
||
--font-height: 0;
|
||
--scale-x: 1;
|
||
--rotate: 0deg;
|
||
|
||
z-index: 1;
|
||
font-size: calc(var(--text-scale-factor) * var(--font-height));
|
||
transform: rotate(var(--rotate)) scaleX(var(--scale-x)) scale(var(--min-font-size-inv));
|
||
}
|
||
|
||
.pdf-text-layer :deep(.markedContent) {
|
||
display: contents;
|
||
}
|
||
|
||
.pdf-text-layer :deep(.is-slice-highlighted) {
|
||
margin: -2px;
|
||
padding: 2px;
|
||
background: rgb(255 202 40 / 48%);
|
||
border-radius: 3px;
|
||
box-shadow: 0 0 0 1px rgb(245 158 11 / 38%);
|
||
}
|
||
|
||
.pdf-text-layer :deep(::selection) {
|
||
color: transparent;
|
||
background: rgb(37 99 235 / 30%);
|
||
}
|
||
|
||
.pdf-loading,
|
||
.pdf-error {
|
||
position: absolute;
|
||
z-index: 3;
|
||
top: 50%;
|
||
left: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
|
||
.pdf-loading {
|
||
gap: 8px;
|
||
padding: 10px 14px;
|
||
color: #f2f4f7;
|
||
font-size: 13px;
|
||
background: rgb(31 36 39 / 86%);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.pdf-error {
|
||
width: min(360px, calc(100% - 32px));
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
padding: 20px;
|
||
color: #667085;
|
||
text-align: center;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 20px rgb(0 0 0 / 22%);
|
||
|
||
i {
|
||
color: #d92d20;
|
||
font-size: 28px;
|
||
}
|
||
|
||
strong {
|
||
color: #344054;
|
||
}
|
||
|
||
span,
|
||
a {
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
.pdf-unavailable {
|
||
display: flex;
|
||
width: 100%;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
color: #667085;
|
||
background: #f8f9fb;
|
||
|
||
i {
|
||
color: #d92d20;
|
||
font-size: 36px;
|
||
}
|
||
|
||
strong {
|
||
color: #344054;
|
||
font-size: 14px;
|
||
}
|
||
|
||
span {
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.pdf-toolbar {
|
||
gap: 5px;
|
||
padding: 0 6px;
|
||
}
|
||
|
||
.location-state {
|
||
font-size: 11px;
|
||
}
|
||
|
||
.pdf-scroll {
|
||
min-height: 420px;
|
||
}
|
||
}
|
||
</style>
|