前端-选中pdf中的文字并使用,显示一个悬浮的翻译按钮(本地pdfjs+iframe)不适用textlayer
使用pdfjs移步–
vue2使用pdfjs-dist实现pdf预览(iframe形式,不修改pdfjs原来的ui和控件,dom层可以用display去掉一部分组件)
- 实现案例
- pdf容器创建,悬浮盒子创建
<iframe
:src="pdfurl"
class="pdfContent"
ref="pdfViewer"
frameborder="0"
width="100%"
height="850px"
></iframe>
<div
v-show="selectionToolsVisible"
class="selection-tools"
:style="selectionPosition"
@mousedown.prevent
>
<div class="tool-item" @click.stop="getAihelper('entocn')">
<img src="../../assets/detailImage/enToCn.png" alt="" />
<span class="tool-text">翻译</span>
</div>
</div>
- data实例
data() {
return {
selectionToolsVisible: false,
selectionPosition: { top: '0px', left: '0px' },
selectionTimer: null,
};
},
- mounted注册鼠标事件-注册pdf的监听
this.$refs.pdfViewer.onload = () => {
const iframeDoc =
this.$refs.pdfViewer.contentDocument ||
this.$refs.pdfViewer.contentWindow.document;
iframeDoc.addEventListener('mouseup', this.handleTextSelectionPdf);
};
- pdf监听代码
handleTextSelectionPdf(e) {
clearTimeout(this.selectionTimer);
// 缓存关键事件属性
const targetElement = e?.target || document.activeElement;
// const cachedSelection = window.getSelection().toString().trim();
this.selectionTimer = setTimeout(() => {
const selection =
this.$refs.pdfViewer.contentWindow.getSelection();
// 增强型六重验证
const isValid =
selection.rangeCount > 0 &&
!selection.isCollapsed &&
selection.toString().trim().length >= 1 && // 允许单字符选择
targetElement.closest('#viewerContainer');
if (isValid) {
// console.log(selection);
const range = selection.getRangeAt(0);
const rect = this.getAdjustedRectPdf(range);
this.updateToolPositionPdf(rect);
this.selectionToolsVisible = true;
this.tempSelection = selection.toString();
} else {
this.selectionToolsVisible = false;
}
}, 50); // 优化响应时间
},
getAdjustedRectPdf(range) {
const tempSpan = document.createElement('span');
range.insertNode(tempSpan);
const rect = tempSpan.getBoundingClientRect();
tempSpan.remove();
// 获取 iframe 在整个页面中的位置
const iframeRect = this.$refs.pdfViewer.getBoundingClientRect();
// **修正点:确保 top 计算正确**
const absoluteTop = rect.top + iframeRect.top;
const absoluteLeft = rect.left + iframeRect.left + window.scrollX;
// console.log(`iframeRect:`, iframeRect);
// console.log(`selection rect:`, rect);
// console.log(
// `absoluteTop: ${absoluteTop}, absoluteLeft: ${absoluteLeft}`
// );
return {
top: absoluteTop,
left: absoluteLeft,
width: rect.width,
height: rect.height,
};
},
updateToolPositionPdf(rect) {
const viewportWidth = window.innerWidth;
const tooltipWidth = '';
this.selectionPosition = {
top: `${rect.top - 70}px`,
left: `${Math.min(
Math.max(rect.left, 10),
viewportWidth - tooltipWidth - 10
)}px`,
maxWidth: `${tooltipWidth}px`,
};
// console.log(`Final tooltip position:`, this.selectionPosition);
},
- 部分样式-按钮样式
<style scoped lang="less">
::v-deep .selection-tools {
position: fixed;
background: rgba(29, 115, 232, 1);
border-radius: 5px;
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.15);
padding: 8px;
display: inline-flex;
align-items: center;
// gap: 6px;
z-index: 9999;
// opacity: 0;
transform: translateY(-10px) scale(0.95);
transition: all 0.1s cubic-bezier(0.4, 0, 0.2, 1);
/* 移除默认的pointer-events限制 */
pointer-events: auto !important;
/* 修正激活状态逻辑 */
&::after {
content: '';
position: absolute;
bottom: -11px;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #1d73e8;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
}
&.active {
opacity: 1;
transform: translateY(0) scale(1);
}
.tool-item {
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
transition: all 0.2s;
flex-direction: column;
height: 50px;
justify-content: space-between;
// &:hover {
// background: #f0f6ff;
// transform: translateY(-1px);
// .iconfont {
// color: #0065cc;
// }
// .tool-text {
// color: #003d82;
// }
// }
.tool-text {
font-family: Microsoft YaHei;
font-weight: 400;
font-size: 12px;
color: #ffffff;
// line-height: 41px;
}
}
.tool-divider {
width: 1px;
height: 38px;
background: #3f86dd;
margin: 0 4px;
}
img {
max-width: 24px;
}
}
</style>