提交 506f41e6 authored 作者: yanpeng's avatar yanpeng

实体制裁文件同步滚动

上级 4d30163e
流水线 #260 已通过 于阶段
in 1 分 26 秒
......@@ -239,8 +239,8 @@ const initNormalGraph = (layoutType, width, height) => {
// 中心节点坐标
const centerX = width / 2;
const centerY = height / 2;
const upperY = centerY - 200; // 上方节点 Y 坐标
const lowerY = centerY + 200; // 下方节点 Y 坐标
const upperY = centerY - 240; // 上方节点 Y 坐标
const lowerY = centerY + 240; // 下方节点 Y 坐标
const nodeSpacing = 100; // 节点水平间距
// 分离中心节点和其他节点
......
<template>
<div class="entity-list">
<div class="header">
<div class="header-title">
<img :src="headerTitle.img" alt="" />
<div>
<div class="title">
{{ headerTitle.title }}
<!-- <span>{{ headerTitle.titleEn }}</span> -->
</div>
<div class="department">
{{ headerTitle.department }}
</div>
</div>
<!-- <div class="btn">
<img :src="icon01" alt="">切换
</div> -->
</div>
</div>
<div class="main">
<div class="pdf-container">
<iframe v-if="headerTitle.srcUrl" :src="headerTitle.srcUrl" width="100%" height="100%" frameborder="0"></iframe>
<div v-else class="no-pdf">暂无原文</div>
</div>
<div class="pdf-container">
<iframe
v-if="headerTitle.transUrl"
:src="headerTitle.transUrl"
width="100%"
height="100%"
frameborder="0"
></iframe>
<div v-else class="no-pdf">暂无译文</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { getSingleSanctionOverview } from "@/api/exportControlV2.0.js";
import title from "../assets/title.png";
import icon01 from "../assets/icon01.png";
// 单次制裁-制裁概况-基本信息
const singleSanctionOverview = ref({});
const getSingleSanctionOverviewData = async () => {
if (!sanRecordId.value) return;
try {
const res = await getSingleSanctionOverview({
sanRecordId: sanRecordId.value
});
if (res.code === 200) {
singleSanctionOverview.value = res.data || {};
// 格式化日期
let dateStr = "";
if (singleSanctionOverview.value.postDate) {
const date = new Date(singleSanctionOverview.value.postDate);
if (!isNaN(date.getTime())) {
dateStr = `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}日`;
} else {
dateStr = singleSanctionOverview.value.postDate;
}
}
// 更新头部信息
headerTitle.value = {
...headerTitle.value,
title: `${dateStr}${singleSanctionOverview.value.sanTitleZh || singleSanctionOverview.value.sanTitle}》`,
titleEn: singleSanctionOverview.value.sanTitle || "",
department: singleSanctionOverview.value.fileCode || "",
srcUrl: singleSanctionOverview.value.srcUrl || "",
transUrl: singleSanctionOverview.value.transUrl || ""
};
}
} catch (error) {
console.error("获取制裁概况失败:", error);
}
};
const headerTitle = ref({
img: title
});
// 获取URL参数
const sanRecordId = ref("");
const getUrlParams = () => {
const urlParams = new URLSearchParams(window.location.search);
sanRecordId.value = urlParams.get("id") || "";
};
onMounted(() => {
getUrlParams();
getSingleSanctionOverviewData();
});
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
}
.entity-list {
width: 100%;
height: 100%;
.header {
width: 100%;
height: 148px;
background-color: #fff;
padding-top: 16px;
.header-title {
width: 1601px;
height: 72px;
background-color: rgba(246, 250, 255, 1);
margin: 0 auto;
border-radius: 10px;
border: 2px solid rgba(174, 214, 255, 1);
display: flex;
align-items: center;
margin-bottom: 12px;
position: relative;
img {
width: 54px;
height: 54px;
margin-left: 15px;
margin-right: 11px;
}
.title {
font-size: 20px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
color: rgb(59, 65, 75);
span {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
margin-left: 11px;
}
}
.department {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
}
.btn {
cursor: pointer;
display: flex;
align-items: center;
position: absolute;
right: 16px;
top: 25px;
font-size: 18px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(5, 95, 194);
img {
width: 20px;
height: 20px;
margin-right: 7px;
}
}
}
.header-nav {
width: 1601px;
margin: 0 auto;
height: 48px;
display: flex;
align-items: center;
.nav-item {
display: flex;
align-items: center;
height: 100%;
margin-right: 32px;
cursor: pointer;
position: relative;
font-size: 18px;
font-weight: 400;
font-family: "Microsoft YaHei";
color: rgb(59, 65, 75);
&:last-child {
margin-right: 0;
}
img {
width: 16px;
height: 16px;
margin-right: 4px;
}
&.active {
color: rgb(5, 95, 194);
font-weight: 700;
}
.active-line {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background-color: #055fc2;
border-radius: 1.5px;
}
}
.original-text-btn {
margin-left: auto;
width: 152px;
height: 36px;
background: #ffffff;
border-radius: 4px;
border: 1px solid rgba(230, 231, 232, 1);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
img {
width: 16px;
height: 16px;
margin-right: 8px;
}
span {
font-size: 16px;
font-weight: 400;
color: rgb(95, 101, 108);
font-family: "Microsoft YaHei";
line-height: 24px;
}
}
}
}
.main {
width: 1601px;
height: calc(100% - 148px);
background-color: #f7f8f9;
margin: 0 auto;
display: flex;
justify-content: space-between;
padding-top: 20px;
box-sizing: border-box;
.pdf-container {
width: 790px;
height: calc(100% - 20px);
background-color: #fff;
// border: 1px solid rgba(174, 214, 255, 1);
border-radius: 4px;
overflow: hidden;
.no-pdf {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #909399;
font-size: 16px;
background-color: #fff;
}
}
}
}
</style>
<template>
<div class="entity-list">
<div class="header">
<div class="header-title">
<img :src="headerTitle.img" alt="">
<div>
<div class="title">
{{ headerTitle.title }}
<!-- <span>{{ headerTitle.titleEn }}</span> -->
</div>
<div class="department">
{{ headerTitle.department }}
</div>
</div>
<!-- <div class="btn">
<img :src="icon01" alt="">切换
</div> -->
</div>
</div>
<div class="main">
<div class="pdf-container">
<iframe v-if="headerTitle.srcUrl" :src="headerTitle.srcUrl" width="100%" height="100%" frameborder="0"></iframe>
<div v-else class="no-pdf">暂无原文</div>
</div>
<div class="pdf-container">
<iframe v-if="headerTitle.transUrl" :src="headerTitle.transUrl" width="100%" height="100%" frameborder="0"></iframe>
<div v-else class="no-pdf">暂无译文</div>
</div>
</div>
</div>
<div class="entity-list">
<div class="header">
<div class="header-title">
<img :src="headerTitle.img" alt="" />
<div>
<div class="title">
{{ headerTitle.title }}
</div>
<div class="department">
{{ headerTitle.department }}
</div>
</div>
</div>
</div>
<div class="main">
<div class="main-header">
<div>实体清单制裁文件</div>
</div>
<!-- 外层滚动容器,统一控制两侧滚动 -->
<div class="report-box" ref="reportBoxRef">
<div class="pdf-pane-wrap">
<pdf ref="leftPdfRef" :pdfUrl="headerTitle.srcUrl" class="pdf-pane-inner" />
</div>
<div class="pdf-pane-wrap">
<pdf ref="rightPdfRef" :pdfUrl="headerTitle.transUrl" class="pdf-pane-inner" />
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getSingleSanctionOverview } from "@/api/exportControlV2.0.js"
import title from "../assets/title.png"
import icon01 from "../assets/icon01.png"
import { ref, onMounted, watch } from "vue";
import { getSingleSanctionOverview } from "@/api/exportControlV2.0.js";
import title from "../assets/title.png";
import pdf from "./pdf.vue";
const leftPdfRef = ref(null);
const rightPdfRef = ref(null);
const reportBoxRef = ref(null);
const headerTitle = ref({
img: title,
title: "",
department: "",
srcUrl: "",
transUrl: ""
});
const sanRecordId = ref("");
const isSyncing = ref(false);
const getUrlParams = () => {
const urlParams = new URLSearchParams(window.location.search);
sanRecordId.value = urlParams.get("id") || "";
};
// 单次制裁-制裁概况-基本信息
const singleSanctionOverview = ref({})
const getSingleSanctionOverviewData = async () => {
if (!sanRecordId.value) return
try {
const res = await getSingleSanctionOverview({
sanRecordId: sanRecordId.value
})
if (res.code === 200) {
singleSanctionOverview.value = res.data || {}
// 格式化日期
let dateStr = "";
if (singleSanctionOverview.value.postDate) {
const date = new Date(singleSanctionOverview.value.postDate);
if (!isNaN(date.getTime())) {
dateStr = `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}日`;
} else {
dateStr = singleSanctionOverview.value.postDate;
}
}
if (!sanRecordId.value) return;
try {
const res = await getSingleSanctionOverview({
sanRecordId: sanRecordId.value
});
if (res.code === 200 && res.data) {
const singleSanctionOverview = res.data || {};
// 更新头部信息
headerTitle.value = {
...headerTitle.value,
title: `${dateStr}${singleSanctionOverview.value.sanTitleZh || singleSanctionOverview.value.sanTitle}》`,
titleEn: singleSanctionOverview.value.sanTitle || "",
department: singleSanctionOverview.value.fileCode || "",
srcUrl: singleSanctionOverview.value.srcUrl || "",
transUrl: singleSanctionOverview.value.transUrl || ""
}
}
} catch (error) {
console.error("获取制裁概况失败:", error)
}
}
// 格式化日期
let dateStr = "";
if (singleSanctionOverview.postDate) {
const date = new Date(singleSanctionOverview.postDate);
if (!isNaN(date.getTime())) {
dateStr = `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}日`;
} else {
dateStr = singleSanctionOverview.postDate;
}
}
// 更新头部信息
headerTitle.value = {
...headerTitle.value,
title: `${dateStr}${singleSanctionOverview.sanTitleZh || singleSanctionOverview.sanTitle}》`,
department: singleSanctionOverview.fileCode || "",
srcUrl: singleSanctionOverview.srcUrl || "",
transUrl: singleSanctionOverview.transUrl || ""
};
}
} catch (error) {
console.error("获取制裁概况失败:", error);
}
};
// 同步滚动处理
const handleSyncScroll = () => {
if (isSyncing.value) return;
isSyncing.value = true;
requestAnimationFrame(() => {
isSyncing.value = false;
});
};
// 监听滚动事件
const setupScrollSync = () => {
const reportBox = reportBoxRef.value;
if (!reportBox) return;
reportBox.addEventListener("scroll", handleSyncScroll, { passive: true });
};
// 监听 PDF 加载完成
watch(
() => [headerTitle.value.srcUrl, headerTitle.value.transUrl],
() => {
// PDF URL 变化时,等待渲染完成后设置滚动监听
setTimeout(() => {
setupScrollSync();
}, 1000);
},
{ deep: true }
);
const headerTitle = ref({
img: title,
})
// 获取URL参数
const sanRecordId = ref("")
const getUrlParams = () => {
const urlParams = new URLSearchParams(window.location.search);
sanRecordId.value = urlParams.get("id") || ""
}
onMounted(() => {
getUrlParams()
getSingleSanctionOverviewData()
})
getUrlParams();
getSingleSanctionOverviewData();
// 等待 DOM 渲染完成后设置滚动监听
setTimeout(() => {
setupScrollSync();
}, 500);
});
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
margin: 0;
padding: 0;
}
.entity-list{
width: 100%;
height: 100%;
.header{
width: 100%;
height: 148px;
background-color: #fff;
padding-top: 16px;
.header-title{
width: 1601px;
height: 72px;
background-color: rgba(246, 250, 255, 1);
margin: 0 auto;
border-radius: 10px;
border: 2px solid rgba(174, 214, 255, 1);
display: flex;
align-items: center;
margin-bottom: 12px;
position: relative;
img {
width: 54px;
height: 54px;
margin-left: 15px;
margin-right: 11px;
}
.title {
font-size: 20px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
color: rgb(59, 65, 75);
span {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
margin-left: 11px;
}
}
.department {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
}
.btn {
cursor: pointer;
display: flex;
align-items: center;
position: absolute;
right: 16px;
top: 25px;
font-size: 18px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(5, 95, 194);
img {
width: 20px;
height: 20px;
margin-right: 7px;
}
}
}
.header-nav {
width: 1601px;
margin: 0 auto;
height: 48px;
display: flex;
align-items: center;
.nav-item {
display: flex;
align-items: center;
height: 100%;
margin-right: 32px;
cursor: pointer;
position: relative;
font-size: 18px;
font-weight: 400;
font-family: "Microsoft YaHei";
color: rgb(59, 65, 75);
&:last-child {
margin-right: 0;
}
.entity-list {
width: 100%;
height: 100%;
overflow-y: auto;
.header {
width: 100%;
height: 148px;
background-color: #fff;
padding-top: 16px;
box-sizing: border-box;
border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
.header-title {
width: 1601px;
height: 72px;
background-color: rgba(246, 250, 255, 1);
margin: 0 auto;
border-radius: 10px;
border: 2px solid rgba(174, 214, 255, 1);
display: flex;
align-items: center;
margin-bottom: 12px;
position: relative;
img {
width: 54px;
height: 54px;
margin-left: 15px;
margin-right: 11px;
}
.title {
font-size: 20px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
color: rgb(59, 65, 75);
}
.department {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
}
}
}
.main {
margin: 0 auto;
background: rgb(255, 255, 255);
width: 1601px;
height: calc(100vh - 148px);
margin-bottom: 20px;
border: 1px solid rgb(234, 236, 238);
box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1);
img {
width: 16px;
height: 16px;
margin-right: 4px;
}
.main-header {
height: 64px;
border-bottom: 1px solid rgb(234, 236, 238);
background: rgb(255, 255, 255);
margin: 0 70px;
color: rgba(59, 65, 75, 1);
font-family: "Source Han Sans CN";
font-size: 20px;
font-weight: 700;
line-height: 26px;
width: 1456px;
text-align: left;
display: flex;
align-items: center;
}
&.active {
color: rgb(5, 95, 194);
font-weight: 700;
}
.report-box {
margin-left: 70px;
width: 1456px;
height: calc(100% - 64px);
display: flex;
overflow-y: auto; /* 统一滚动条,控制两侧一起滚动 */
overflow-x: hidden;
}
.active-line {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background-color: #055fc2;
border-radius: 1.5px;
}
}
.pdf-pane-wrap {
flex: 0 0 50%;
max-width: 50%;
height: 100%;
min-width: 0;
}
.original-text-btn {
margin-left: auto;
width: 152px;
height: 36px;
background: #FFFFFF;
border-radius: 4px;
border: 1px solid rgba(230, 231, 232, 1);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
img {
width: 16px;
height: 16px;
margin-right: 8px;
}
span {
font-size: 16px;
font-weight: 400;
color: rgb(95, 101, 108);
font-family: "Microsoft YaHei";
line-height: 24px;
}
}
}
}
.main{
width: 1601px;
height: calc(100% - 148px);
background-color: #F7F8F9;
margin: 0 auto;
display: flex;
justify-content: space-between;
padding-top: 20px;
box-sizing: border-box;
.pdf-container {
width: 790px;
height: calc(100% - 20px);
background-color: #fff;
// border: 1px solid rgba(174, 214, 255, 1);
border-radius: 4px;
overflow: hidden;
.no-pdf {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #909399;
font-size: 16px;
background-color: #fff;
}
}
}
.pdf-pane-inner {
width: 100%;
height: 100%;
}
}
}
</style>
\ No newline at end of file
</style>
<template>
<div class="pdf-viewer">
<!-- PDF 页面:canvas + textLayer 必须在同一容器内渲染 -->
<div class="page-wrap" v-for="page in pageCount" :key="page">
<canvas :ref="el => setCanvasRef(page, el)"></canvas>
<div :ref="el => setOverlayRef(page, el)" class="textLayer"></div>
</div>
<div v-if="loading" class="loading">加载中...</div>
</div>
</template>
<script>
import { ref, shallowRef, nextTick, watch } from "vue";
import * as pdfjsLib from "pdfjs-dist/legacy/build/pdf";
import PdfWorker from "pdfjs-dist/legacy/build/pdf.worker.min?worker";
// 使用 Vite 的 ?worker 直接注入 Worker,避免线上 mjs MIME 类型问题
pdfjsLib.GlobalWorkerOptions.workerPort = new PdfWorker();
export default {
name: "PdfViewer",
props: {
pdfUrl: {
type: String,
required: true
}
},
setup(props) {
const canvasMap = {};
const overlayMap = {};
const pageCount = ref(0);
const loading = ref(true);
const renderedPageCount = ref(0);
let resolveRenderAll = null;
const waitAllPagesRendered = () => {
if (pageCount.value > 0 && renderedPageCount.value >= pageCount.value) {
return Promise.resolve();
}
return new Promise(resolve => {
resolveRenderAll = resolve;
});
};
// pdfjs 的 document 对象内部使用 #private 字段,用 shallowRef 保持为原始对象引用
const pdfDocRef = shallowRef(null);
const searchKey = ref("");
const matchList = ref([]);
const matchIdx = ref(0);
const pdfjsApiRef = shallowRef(pdfjsLib);
// 保存 canvas
const setCanvasRef = (page, el) => {
if (!el) return;
canvasMap[page] = el;
};
// 保存 textLayer 容器(用于搜索高亮)
const setOverlayRef = (page, el) => {
if (!el) return;
overlayMap[page] = el;
};
// 清理 URL
const parsePdfUrl = pdfUrl => {
if (!pdfUrl || typeof pdfUrl !== "string") return "";
const [urlPart] = pdfUrl.split("#");
return urlPart;
};
// 清空所有高亮
const clearHighlights = () => {
Object.values(overlayMap).forEach(layer => {
if (!layer) return;
const rects = layer.querySelectorAll(".highlight-rect");
rects.forEach(n => n.remove());
});
};
// 重置搜索状态
const clearSearch = () => {
searchKey.value = "";
matchList.value = [];
matchIdx.value = 0;
clearHighlights();
};
// 渲染单页 PDF
const renderPage = async (pdf, pageNum) => {
const pdfPage = await pdf.getPage(pageNum);
const canvas = canvasMap[pageNum];
const textLayer = overlayMap[pageNum];
if (!canvas || !textLayer) return;
const baseViewport = pdfPage.getViewport({ scale: 1 });
const desiredWidth = canvas.clientWidth || 726;
const scale = desiredWidth / baseViewport.width;
const viewport = pdfPage.getViewport({ scale });
const context = canvas.getContext("2d");
canvas.width = viewport.width;
canvas.height = viewport.height;
canvas.style.width = `${viewport.width}px`;
canvas.style.height = `${viewport.height}px`;
textLayer.style.width = canvas.width + "px";
textLayer.style.height = canvas.height + "px";
textLayer.innerHTML = "";
textLayer.style.setProperty("--scale-factor", String(viewport.scale || 1));
await pdfPage.render({ canvasContext: context, viewport }).promise;
try {
const textContent = await pdfPage.getTextContent();
let api = pdfjsApiRef.value || pdfjsLib;
let rt = api?.renderTextLayer;
if (typeof rt !== "function") {
try {
const legacy = await import("pdfjs-dist/legacy/build/pdf");
pdfjsApiRef.value = legacy;
api = legacy;
rt = legacy?.renderTextLayer;
} catch (_) {}
}
if (typeof rt === "function") {
await rt({
textContent,
container: textLayer,
viewport,
textDivs: [],
enhanceTextSelection: false
}).promise;
}
} catch (e) {
console.warn("textLayer 渲染失败", e);
}
renderedPageCount.value += 1;
if (pageCount.value > 0 && renderedPageCount.value >= pageCount.value) {
if (typeof resolveRenderAll === "function") {
const fn = resolveRenderAll;
resolveRenderAll = null;
fn();
}
}
};
// 渲染 PDF
const renderPdf = async pdfUrl => {
const url = parsePdfUrl(pdfUrl);
if (!url) return;
loading.value = true;
pdfDocRef.value = null;
clearHighlights();
matchList.value = [];
searchKey.value = "";
renderedPageCount.value = 0;
resolveRenderAll = null;
try {
const pdf = await pdfjsLib.getDocument(url).promise;
pdfDocRef.value = pdf;
pageCount.value = pdf.numPages;
await nextTick();
for (let p = 1; p <= pdf.numPages; p++) {
await renderPage(pdf, p);
}
} catch (err) {
console.error("PDF 加载失败", err);
} finally {
loading.value = false;
}
};
// 搜索关键词 + 高亮
const doSearch = async () => {
const doc = pdfDocRef.value;
const key = searchKey.value.trim();
clearHighlights();
matchList.value = [];
matchIdx.value = 0;
if (!doc || !key) return;
await waitAllPagesRendered();
for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
const layer = overlayMap[pageNum];
if (!layer) continue;
const nodes = Array.from(layer.querySelectorAll("span"));
for (const el of nodes) {
const t = el.textContent || "";
if (!t) continue;
let start = 0;
while (true) {
const idx = t.indexOf(key, start);
if (idx === -1) break;
matchList.value.push({ pageNum, el, startIdx: idx, endIdx: idx + key.length });
start = idx + Math.max(1, key.length);
}
}
}
if (matchList.value.length > 0) jumpTo(0);
};
// 跳转到第 N 个匹配项
const jumpTo = idx => {
if (idx < 0 || idx >= matchList.value.length) return;
matchIdx.value = idx;
const m = matchList.value[idx];
const el = m?.el;
if (!el) return;
clearHighlights();
const textNode = el.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
try {
const range = document.createRange();
range.setStart(textNode, Math.max(0, m.startIdx ?? 0));
range.setEnd(textNode, Math.max(0, m.endIdx ?? 0));
const rectList = Array.from(range.getClientRects());
const pageWrap = el.closest(".page-wrap");
const layer = overlayMap[m.pageNum];
if (pageWrap && layer && rectList.length) {
const pageRect = pageWrap.getBoundingClientRect();
rectList.forEach(r => {
const mark = document.createElement("div");
mark.className = "highlight-rect";
mark.style.left = r.left - pageRect.left + "px";
mark.style.top = r.top - pageRect.top + "px";
mark.style.width = r.width + "px";
mark.style.height = r.height + "px";
layer.appendChild(mark);
});
}
range.detach?.();
} catch (e) {
// ignore
}
}
// 滚动到匹配位置
const container = el.closest(".report-box");
if (container) {
const TOP_OFFSET = 72;
const containerRect = container.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
const targetTop = elRect.top - containerRect.top + container.scrollTop - TOP_OFFSET;
container.scrollTo({ top: Math.max(0, targetTop), behavior: "smooth" });
} else {
el.scrollIntoView({ behavior: "smooth", block: "center" });
}
};
const prevMatch = () => jumpTo(matchIdx.value - 1);
const nextMatch = () => jumpTo(matchIdx.value + 1);
const getMatchInfo = () => {
const total = matchList.value.length;
const current = total ? matchIdx.value + 1 : 0;
return { current, total };
};
// 外部调用方法
const searchKeyword = async keyword => {
searchKey.value = keyword;
await doSearch();
return matchList.value.length > 0 ? matchList.value[0].pageNum : 0;
};
const goToPage = pageNum => {
const canvasEl = canvasMap[pageNum];
if (!canvasEl) return;
const container = canvasEl.closest(".report-box");
if (container) {
const containerRect = container.getBoundingClientRect();
const canvasRect = canvasEl.getBoundingClientRect();
const targetTop = canvasRect.top - containerRect.top + container.scrollTop;
container.scrollTo({ top: Math.max(0, targetTop), behavior: "smooth" });
} else {
canvasEl.scrollIntoView({ behavior: "smooth", block: "start" });
}
};
// 获取容器元素(用于同步滚动)
const getContainer = () => {
const firstCanvas = canvasMap[1];
return firstCanvas ? firstCanvas.closest(".report-box") : null;
};
watch(
() => props.pdfUrl,
newVal => {
if (newVal) renderPdf(newVal);
},
{ immediate: true }
);
return {
pageCount,
setCanvasRef,
setOverlayRef,
loading,
searchKey,
doSearch,
prevMatch,
nextMatch,
getMatchInfo,
matchList,
matchIdx,
searchKeyword,
clearSearch,
goToPage,
getContainer
};
}
};
</script>
<style scoped>
.pdf-viewer {
position: relative;
width: 100%;
}
.page-wrap {
position: relative;
margin-bottom: 16px;
width: 100%;
}
canvas {
width: 100%;
height: auto;
display: block;
}
.textLayer {
position: absolute;
left: 0;
top: 0;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 2;
line-height: 1;
}
.textLayer :deep(span) {
position: absolute;
transform-origin: 0% 0%;
white-space: pre;
line-height: 1;
font-size: calc(var(--font-height, 0px) * var(--scale-factor, 1));
transform: scaleX(var(--scale-x, 1));
color: transparent;
}
.textLayer :deep(.highlight-text) {
background: #ff0;
opacity: 0.6;
padding: 0 1px;
border-radius: 2px;
}
.textLayer :deep(.highlight-rect) {
position: absolute;
background: #ff0;
opacity: 0.6;
border-radius: 2px;
pointer-events: none;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 18px;
color: #333;
}
</style>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论