Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
P
pic-reader
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
龙菲
pic-reader
Commits
8b4b0a55
提交
8b4b0a55
authored
8月 14, 2025
作者:
gzcnkilys_admin
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
1、pic reader增加pdf文件加载,支持文本渲染
上级
06968f64
隐藏空白字符变更
内嵌
并排
正在显示
2 个修改的文件
包含
747 行增加
和
9 行删除
+747
-9
pdf.worker.min.mjs
public/pdf.worker.min.mjs
+0
-0
index.vue
src/components/BookReader/index.vue
+747
-9
没有找到文件。
public/pdf.worker.min.mjs
0 → 100644
浏览文件 @
8b4b0a55
This source diff could not be displayed because it is too large. You can
view the blob
instead.
src/components/BookReader/index.vue
浏览文件 @
8b4b0a55
<
template
>
<div
class=
"book-reader"
v-loading=
"
loading && initial
Loading"
element-loading-text=
"正在加载图书...
"
v-loading=
"
(loading && initialLoading) || pdf
Loading"
:element-loading-text=
"props.isPdf ? '正在加载PDF...' : '正在加载图书...'
"
>
<div
class=
"magazine-viewport"
>
<div
ref=
"magazine"
class=
"magazine"
>
...
...
@@ -20,7 +20,17 @@
},
]"
>
<!-- PDF页面使用canvas -->
<canvas
v-if=
"page.isPdf"
:data-page-index=
"index"
:class=
"['page-canvas',
{ 'wide-canvas': page.isWide }]"
:ref="`canvas${index}`"
>
</canvas>
<!-- 图片页面使用img -->
<img
v-else
:data-page-index=
"index"
:src=
"page.src"
:alt=
"`第 $
{page.page_num} 页`"
...
...
@@ -145,6 +155,33 @@
</el-tooltip>
</el-col>
<el-col
class=
"right"
:span=
"8"
>
<!-- PDF文字层开关 -->
<el-tooltip
v-if=
"props.isPdf"
:content=
"textLayerEnabled ? '关闭文字层' : '开启文字层'"
placement=
"top"
effect=
"dark"
>
<svg-icon
:name=
"textLayerEnabled ? 'list' : 'menu'"
:size=
"20"
@
click=
"toggleTextLayer"
></svg-icon>
</el-tooltip>
<!-- PDF清晰度开关 -->
<el-tooltip
v-if=
"props.isPdf"
:content=
"props.highQualityRendering ? '降低清晰度' : '提高清晰度'"
placement=
"top"
effect=
"dark"
>
<svg-icon
:name=
"props.highQualityRendering ? 'zoom-in' : 'zoom-out'"
:size=
"20"
@
click=
"toggleHighQuality"
></svg-icon>
</el-tooltip>
<!--
<svg-icon
name=
"download"
size=
"20"
></svg-icon>
-->
<!--
<svg-icon
name=
"fullscreen"
size=
"20"
></svg-icon>
-->
</el-col>
...
...
@@ -155,11 +192,12 @@
<
script
setup
>
import
$
from
"jquery"
;
import
"@/assets/js/turn.js"
;
// 直接导入执行,扩展 jQuery 对象
import
{
ref
,
onMounted
,
onUnmounted
,
watch
,
nextTick
,
computed
}
from
"vue"
;
import
{
ref
,
onMounted
,
onUnmounted
,
watch
,
nextTick
,
computed
,
toRaw
}
from
"vue"
;
import
VueEasyLightbox
from
"vue-easy-lightbox"
;
import
GuideMobile
from
"../GuideMobile/index.vue"
;
import
audioFlip
from
"@/assets/audio/flip.mp3"
;
import
throttle
from
"lodash/throttle"
;
import
*
as
pdfjsLib
from
"pdfjs-dist"
;
const
props
=
defineProps
({
pages
:
{
type
:
Array
,
...
...
@@ -170,6 +208,23 @@ const props = defineProps({
type
:
Array
,
default
:
()
=>
[],
},
// 新增PDF相关属性
pdfUrl
:
{
type
:
String
,
default
:
""
,
},
isPdf
:
{
type
:
Boolean
,
default
:
false
,
},
enableTextLayer
:
{
type
:
Boolean
,
default
:
true
,
},
highQualityRendering
:
{
type
:
Boolean
,
default
:
true
,
},
});
const
magazine
=
ref
(
null
);
...
...
@@ -198,6 +253,15 @@ const previewImages = ref([]);
// 将 processedPages 改为普通数据属性
const
processedPages
=
ref
([]);
// PDF相关状态
const
pdfDocument
=
ref
(
null
);
const
totalPdfPages
=
ref
(
0
);
const
pdfLoading
=
ref
(
false
);
const
textLayerEnabled
=
ref
(
props
.
enableTextLayer
);
// 文字层开关状态
// const textLayerEnabled = ref(false); // 文字层开关状态
const
renderedPages
=
ref
(
new
Set
());
// 跟踪已渲染的页面
const
pdfInitialized
=
ref
(
false
);
// 跟踪PDF是否已完成初始化渲染
// Add new refs for sound and directory
const
pageTurnSound
=
ref
(
null
);
const
audioInitialized
=
ref
(
false
);
...
...
@@ -229,6 +293,21 @@ const processPages = (pages) => {
}));
};
// 处理PDF页面
const
processPdfPages
=
(
totalPages
)
=>
{
return
Array
.
from
({
length
:
totalPages
},
(
_
,
index
)
=>
({
src
:
""
,
originalIndex
:
index
,
page_num
:
index
+
1
,
images
:
[],
isWide
:
false
,
isLoaded
:
false
,
url
:
""
,
// PDF页面不需要URL
isPdf
:
true
,
pageIndex
:
index
,
}));
};
const
loadImage
=
async
(
pageIndex
)
=>
{
const
page
=
processedPages
.
value
[
pageIndex
];
if
(
!
page
||
page
.
isLoaded
||
preloadingPages
.
value
.
has
(
pageIndex
))
{
...
...
@@ -327,6 +406,48 @@ const loadVisiblePages = async (currentPageNum) => {
// 完全移除finally块中的loading状态管理
};
// 渲染可见的PDF页面
const
renderVisiblePdfPages
=
async
(
currentPageNum
)
=>
{
if
(
!
magazine
.
value
||
!
pdfDocument
.
value
)
return
;
// 避免在PDF加载过程中重复渲染
if
(
pdfLoading
.
value
)
{
console
.
log
(
`PDF正在加载中,跳过渲染页面:
${
currentPageNum
}
`
);
return
;
}
const
$magazine
=
$
(
magazine
.
value
);
const
totalPages
=
$magazine
.
turn
(
"pages"
);
// 渲染当前页、上一页、下一页
const
startPage
=
Math
.
max
(
1
,
currentPageNum
-
1
);
const
endPage
=
Math
.
min
(
totalPages
,
currentPageNum
+
1
);
const
highPriority
=
[
currentPageNum
,
currentPageNum
+
1
];
// 优先渲染当前和下一页
console
.
log
(
`渲染PDF页面:
${
currentPageNum
}
, 范围:
${
startPage
}
-
${
endPage
}
`
);
try
{
console
.
log
(
`开始渲染高优先级页面:
${
highPriority
.
join
(
', '
)}
`
);
// 优先渲染高优先级页面
await
Promise
.
allSettled
(
highPriority
.
map
((
pageIndex
)
=>
renderPdfPage
(
pageIndex
-
1
))
);
// 后台渲染其他页面,不阻塞UI
const
otherPages
=
Array
.
from
({
length
:
endPage
-
startPage
+
1
},
(
_
,
i
)
=>
startPage
+
i
)
.
filter
((
page
)
=>
!
highPriority
.
includes
(
page
));
if
(
otherPages
.
length
>
0
)
{
console
.
log
(
`后台渲染其他页面:
${
otherPages
.
join
(
', '
)}
`
);
Promise
.
allSettled
(
otherPages
.
map
((
pageIndex
)
=>
renderPdfPage
(
pageIndex
-
1
))
);
}
}
catch
(
error
)
{
console
.
error
(
"渲染PDF页面失败:"
,
error
);
}
};
// 修改初始化函数
const
initBook
=
async
()
=>
{
if
(
!
magazine
.
value
||
!
processedPages
.
value
.
length
)
return
;
...
...
@@ -387,6 +508,26 @@ const initBook = async () => {
objectFit
:
"contain"
,
});
}
// 处理PDF页面的canvas
const
$canvas
=
$page
.
find
(
"canvas"
);
if
(
$canvas
.
length
)
{
$canvas
.
css
({
width
:
"100%"
,
height
:
"100%"
,
display
:
"block"
,
});
}
// 确保PDF页面容器有正确的尺寸
if
(
$page
.
find
(
"canvas"
).
length
)
{
$page
.
css
({
width
:
pageWidth
,
height
:
pageHeight
,
minWidth
:
pageWidth
,
minHeight
:
pageHeight
,
});
}
});
// 移动端使用单页显示
...
...
@@ -396,7 +537,7 @@ const initBook = async () => {
display
:
isMobile
.
value
?
"single"
:
"double"
,
acceleration
:
true
,
// 启用非corner区域翻页
enableNonCorner
:
tru
e
,
enableNonCorner
:
fals
e
,
// 最大折叠距离比例 (0.1 - 1.0)
maxFoldingDistance
:
2
,
// 最小拖拽距离触发翻页
...
...
@@ -414,9 +555,22 @@ const initBook = async () => {
},
turned
:
(
event
,
page
)
=>
{
currentPage
.
value
=
page
;
console
.
log
(
`turn.js翻页到第
${
page
}
页`
);
// 翻页完成后异步预加载,不影响动画,不设置翻页锁
setTimeout
(()
=>
{
loadVisiblePages
(
page
);
if
(
props
.
isPdf
&&
pdfDocument
.
value
)
{
// 避免在初始化时重复渲染
if
(
isInitialized
.
value
&&
!
pdfLoading
.
value
&&
pdfInitialized
.
value
)
{
console
.
log
(
`开始渲染第
${
page
}
页的PDF内容`
);
renderVisiblePdfPages
(
page
);
}
}
else
{
loadVisiblePages
(
page
);
}
// 调试:检查页面状态
setTimeout
(()
=>
debugPageStatus
(),
100
);
},
50
);
// 播放声音
...
...
@@ -429,7 +583,11 @@ const initBook = async () => {
// 初始化完成后关闭loading
loading
.
value
=
false
;
initialLoading
.
value
=
false
;
await
loadVisiblePages
(
1
);
// 只处理图片页面,PDF页面在loadPdf中处理
if
(
!
props
.
isPdf
)
{
await
loadVisiblePages
(
1
);
}
}
catch
(
error
)
{
console
.
error
(
"Turn.js initialization error:"
,
error
);
loading
.
value
=
false
;
...
...
@@ -440,7 +598,10 @@ const initBook = async () => {
const
loadImages
=
async
()
=>
{
// loading.value = true;
try
{
await
initBook
();
// 只处理图片页面,不初始化turn.js(PDF模式下已经初始化)
if
(
!
props
.
isPdf
)
{
await
initBook
();
}
}
catch
(
error
)
{
console
.
error
(
"加载图片失败:"
,
error
);
}
finally
{
...
...
@@ -448,11 +609,62 @@ const loadImages = async () => {
}
};
// 加载PDF
const
loadPdf
=
async
()
=>
{
if
(
!
props
.
pdfUrl
)
{
console
.
error
(
"PDF URL 未提供"
);
return
;
}
try
{
pdfLoading
.
value
=
true
;
console
.
log
(
"开始加载PDF:"
,
props
.
pdfUrl
);
// 加载PDF文档
const
loadingTask
=
pdfjsLib
.
getDocument
(
props
.
pdfUrl
);
const
rawPdfDocument
=
await
loadingTask
.
promise
;
pdfDocument
.
value
=
rawPdfDocument
;
totalPdfPages
.
value
=
rawPdfDocument
.
numPages
;
console
.
log
(
"PDF加载成功,总页数:"
,
totalPdfPages
.
value
);
// 处理PDF页面
processedPages
.
value
=
processPdfPages
(
totalPdfPages
.
value
);
// 清除之前的渲染状态
renderedPages
.
value
.
clear
();
pdfInitialized
.
value
=
false
;
// 重置PDF初始化状态
// 初始化turn.js
await
initBook
();
// 先关闭loading状态,再渲染第一页PDF
pdfLoading
.
value
=
false
;
// 等待turn.js完全初始化完成
await
new
Promise
(
resolve
=>
setTimeout
(
resolve
,
200
));
// 渲染第一页PDF
await
renderVisiblePdfPages
(
1
);
// 标记PDF已完成初始化渲染
pdfInitialized
.
value
=
true
;
// 等待一小段时间,确保turn.js事件处理完成
await
new
Promise
(
resolve
=>
setTimeout
(
resolve
,
100
));
console
.
log
(
"PDF渲染完成"
);
}
catch
(
error
)
{
console
.
error
(
"PDF加载失败:"
,
error
);
pdfLoading
.
value
=
false
;
}
};
// 监听 props.pages 的变化
watch
(
()
=>
props
.
pages
,
(
newPages
)
=>
{
if
(
newPages
&&
newPages
.
length
>
0
)
{
if
(
newPages
&&
newPages
.
length
>
0
&&
!
props
.
isPdf
)
{
processedPages
.
value
=
processPages
(
newPages
);
loadImages
();
}
...
...
@@ -460,6 +672,17 @@ watch(
{
immediate
:
true
}
);
// 监听PDF URL的变化
watch
(
()
=>
props
.
pdfUrl
,
async
(
newUrl
)
=>
{
if
(
newUrl
&&
props
.
isPdf
)
{
console
.
log
(
"PDF URL 变化,重新加载:"
,
newUrl
);
await
loadPdf
();
}
}
);
const
destroyTurn
=
()
=>
{
if
(
magazine
.
value
&&
isInitialized
.
value
)
{
try
{
...
...
@@ -476,6 +699,350 @@ const destroyTurn = () => {
// 初始化 Intersection Observer
// 渲染单个PDF页面
const
renderPdfPage
=
async
(
pageIndex
)
=>
{
if
(
!
pdfDocument
.
value
||
!
magazine
.
value
)
return
;
let
pageNum
=
pageIndex
+
1
// 检查页面是否已经渲染过
if
(
renderedPages
.
value
.
has
(
pageIndex
))
{
console
.
log
(
`页面
${
pageNum
}
已经渲染过,跳过`
);
return
;
}
console
.
log
(
`开始渲染PDF页面
${
pageNum
}
`
);
try
{
// 使用 toRaw 将 Vue3 响应式对象转换为原始对象,避免 Proxy 兼容性问题
const
rawPdfDocument
=
toRaw
(
pdfDocument
.
value
);
const
page
=
await
rawPdfDocument
.
getPage
(
pageNum
);
// 获取页面容器
const
pageContainer
=
magazine
.
value
.
querySelector
(
`.p
${
pageNum
}
`
);
if
(
!
pageContainer
)
{
console
.
warn
(
`Page container not found for page
${
pageNum
}
`
);
return
;
}
// 检查是否已经渲染过
let
canvas
=
pageContainer
.
querySelector
(
'canvas'
);
let
isNewCanvas
=
false
;
// 如果canvas已经渲染过,创建新的canvas
if
(
canvas
&&
canvas
.
dataset
.
rendered
===
'true'
)
{
const
newCanvas
=
document
.
createElement
(
'canvas'
);
newCanvas
.
className
=
canvas
.
className
;
newCanvas
.
style
.
cssText
=
canvas
.
style
.
cssText
;
pageContainer
.
replaceChild
(
newCanvas
,
canvas
);
canvas
=
newCanvas
;
isNewCanvas
=
true
;
}
if
(
!
canvas
)
{
console
.
warn
(
`Canvas not found for page
${
pageNum
}
`
);
return
;
}
// 计算缩放比例(考虑设备像素比,提高清晰度)
const
viewport
=
page
.
getViewport
({
scale
:
1.0
});
// 获取容器尺寸,如果容器尺寸为0,则使用turn.js设置的页面尺寸
let
containerWidth
=
canvas
.
clientWidth
||
pageContainer
.
clientWidth
;
let
containerHeight
=
canvas
.
clientHeight
||
pageContainer
.
clientHeight
;
// 如果容器尺寸为0,尝试从turn.js获取页面尺寸
if
(
containerWidth
===
0
||
containerHeight
===
0
)
{
const
$magazine
=
$
(
magazine
.
value
);
if
(
$magazine
.
length
&&
$magazine
.
turn
)
{
const
turnSize
=
$magazine
.
turn
(
'size'
);
if
(
turnSize
&&
turnSize
.
width
&&
turnSize
.
height
)
{
// 移动端使用单页尺寸,桌面端使用双页尺寸的一半
if
(
isMobile
.
value
)
{
containerWidth
=
turnSize
.
width
;
containerHeight
=
turnSize
.
height
;
}
else
{
containerWidth
=
turnSize
.
width
/
2
;
containerHeight
=
turnSize
.
height
;
}
}
}
}
// 如果仍然无法获取尺寸,使用默认尺寸
if
(
containerWidth
===
0
||
containerHeight
===
0
)
{
console
.
warn
(
`页面
${
pageNum
}
容器尺寸为0,使用默认尺寸`
);
const
viewportWidth
=
window
.
innerWidth
;
const
viewportHeight
=
window
.
innerHeight
;
if
(
isMobile
.
value
)
{
containerWidth
=
viewportWidth
-
20
;
containerHeight
=
(
containerWidth
*
4
)
/
3
;
if
(
containerHeight
>
viewportHeight
-
100
)
{
containerHeight
=
viewportHeight
-
92
;
containerWidth
=
(
containerHeight
*
3
)
/
4
;
}
}
else
{
containerWidth
=
Math
.
min
(
600
,
viewportWidth
/
2
-
50
);
containerHeight
=
Math
.
min
(
800
,
viewportHeight
-
100
);
}
}
console
.
log
(
`页面
${
pageNum
}
容器尺寸:
${
containerWidth
}
x
${
containerHeight
}
`
);
// 获取设备像素比,提高高分辨率屏幕的显示质量
const
devicePixelRatio
=
window
.
devicePixelRatio
||
1
;
const
scaleX
=
containerWidth
/
viewport
.
width
;
const
scaleY
=
containerHeight
/
viewport
.
height
;
const
baseScale
=
Math
.
min
(
scaleX
,
scaleY
);
// 根据清晰度设置决定是否使用高分辨率
const
scale
=
props
.
highQualityRendering
?
baseScale
*
devicePixelRatio
:
baseScale
;
// 设置canvas尺寸(使用高分辨率)
const
scaledViewport
=
page
.
getViewport
({
scale
});
canvas
.
width
=
Math
.
floor
(
scaledViewport
.
width
);
canvas
.
height
=
Math
.
floor
(
scaledViewport
.
height
);
// 设置CSS尺寸(保持显示大小不变)
canvas
.
style
.
width
=
`
${
containerWidth
}
px`
;
canvas
.
style
.
height
=
`
${
containerHeight
}
px`
;
console
.
log
(
`页面
${
pageIndex
+
1
}
缩放信息: 基础缩放=
${
baseScale
.
toFixed
(
3
)}
, 设备像素比=
${
devicePixelRatio
}
, 最终缩放=
${
scale
.
toFixed
(
3
)}
, canvas尺寸=
${
canvas
.
width
}
x
${
canvas
.
height
}
, 显示尺寸=
${
containerWidth
}
x
${
containerHeight
}
`
);
// 渲染PDF页面
const
context
=
canvas
.
getContext
(
"2d"
);
const
renderContext
=
{
canvasContext
:
context
,
viewport
:
scaledViewport
,
};
await
page
.
render
(
renderContext
).
promise
;
// 标记canvas已渲染
canvas
.
dataset
.
rendered
=
'true'
;
// 标记页面已渲染
renderedPages
.
value
.
add
(
pageIndex
);
// 渲染文字层(如果启用)
if
(
textLayerEnabled
.
value
)
{
try
{
const
textContent
=
await
page
.
getTextContent
();
// 创建文字层容器
const
textLayer
=
document
.
createElement
(
'div'
);
textLayer
.
className
=
'text-layer'
;
textLayer
.
id
=
`text-layer-
${
pageNum
}
`
;
// 添加唯一ID便于调试
textLayer
.
style
.
position
=
'absolute'
;
textLayer
.
style
.
left
=
'0'
;
textLayer
.
style
.
top
=
'0'
;
// 文字层尺寸应该和turn.js的页面尺寸一致,这样就能完全覆盖左右分页
// 从turn.js获取实际的页面尺寸
const
$magazine
=
$
(
magazine
.
value
);
let
turnPageWidth
,
turnPageHeight
;
if
(
$magazine
.
length
&&
$magazine
.
turn
)
{
const
turnSize
=
$magazine
.
turn
(
'size'
);
if
(
turnSize
&&
turnSize
.
width
&&
turnSize
.
height
)
{
// 移动端使用单页尺寸,桌面端使用双页尺寸的一半
if
(
isMobile
.
value
)
{
turnPageWidth
=
turnSize
.
width
;
turnPageHeight
=
turnSize
.
height
;
}
else
{
turnPageWidth
=
turnSize
.
width
/
2
;
turnPageHeight
=
turnSize
.
height
;
}
}
}
// 如果无法从turn.js获取尺寸,使用containerWidth和containerHeight
if
(
!
turnPageWidth
||
!
turnPageHeight
)
{
turnPageWidth
=
containerWidth
;
turnPageHeight
=
containerHeight
;
}
textLayer
.
style
.
width
=
`
${
turnPageWidth
}
px`
;
textLayer
.
style
.
height
=
`
${
turnPageHeight
}
px`
;
textLayer
.
style
.
pointerEvents
=
'auto'
;
textLayer
.
style
.
userSelect
=
'text'
;
textLayer
.
style
.
zIndex
=
'999'
;
// 使用更高的z-index
textLayer
.
style
.
backgroundColor
=
'transparent'
;
// 文字层背景透明
// 添加详细的尺寸调试信息
console
.
log
(
`=== 页面
${
pageNum
}
尺寸调试 ===`
);
console
.
log
(
`containerWidth:
${
containerWidth
}
, containerHeight:
${
containerHeight
}
`
);
console
.
log
(
`turn.js页面尺寸:
${
turnPageWidth
}
x
${
turnPageHeight
}
`
);
console
.
log
(
`canvas.style.width:
${
canvas
.
style
.
width
}
, canvas.style.height:
${
canvas
.
style
.
height
}
`
);
console
.
log
(
`canvas.clientWidth:
${
canvas
.
clientWidth
}
, canvas.clientHeight:
${
canvas
.
clientHeight
}
`
);
console
.
log
(
`canvas.offsetWidth:
${
canvas
.
offsetWidth
}
, canvas.offsetHeight:
${
canvas
.
offsetHeight
}
`
);
console
.
log
(
`pageContainer.clientWidth:
${
pageContainer
.
clientWidth
}
, pageContainer.clientHeight:
${
pageContainer
.
clientHeight
}
`
);
console
.
log
(
`PDF原始尺寸:
${
viewport
.
width
}
x
${
viewport
.
height
}
`
);
console
.
log
(
`缩放比例:
${
baseScale
}
`
);
console
.
log
(
`文字层尺寸:
${
turnPageWidth
}
x
${
turnPageHeight
}
`
);
console
.
log
(
`========================`
);
// 专门打印文字层和PDF分页的尺寸对比
console
.
log
(
`🔍 尺寸对比检查:`
);
console
.
log
(
` 文字层尺寸:
${
turnPageWidth
}
x
${
turnPageHeight
}
`
);
console
.
log
(
` PDF分页尺寸:
${
pageContainer
.
clientWidth
}
x
${
pageContainer
.
clientHeight
}
`
);
console
.
log
(
` Canvas显示尺寸:
${
canvas
.
style
.
width
}
x
${
canvas
.
style
.
height
}
`
);
console
.
log
(
` 是否一致:
${
turnPageWidth
===
pageContainer
.
clientWidth
&&
turnPageHeight
===
pageContainer
.
clientHeight
?
'✅ 一致'
:
'❌ 不一致'
}
`
);
if
(
turnPageWidth
!==
pageContainer
.
clientWidth
||
turnPageHeight
!==
pageContainer
.
clientHeight
)
{
console
.
log
(
` 差异: 宽度差
${
Math
.
abs
(
turnPageWidth
-
pageContainer
.
clientWidth
)}
px, 高度差
${
Math
.
abs
(
turnPageHeight
-
pageContainer
.
clientHeight
)}
px`
);
}
// 不需要scale变换,直接使用turn.js的页面尺寸
textLayer
.
style
.
transform
=
'none'
;
textLayer
.
style
.
transformOrigin
=
'0 0'
;
console
.
log
(
`文字层直接使用turn.js页面尺寸,无变换`
);
// 添加文字项
textContent
.
items
.
forEach
((
item
,
index
)
=>
{
// 简化文字定位,先确保能看到文字
const
style
=
textContent
.
styles
[
item
.
fontName
];
const
fontSize
=
item
.
height
*
baseScale
;
// 应用缩放
const
fontFamily
=
style
?
style
.
fontFamily
:
'sans-serif'
;
// 创建文字span元素
const
textSpan
=
document
.
createElement
(
'span'
);
textSpan
.
textContent
=
item
.
str
;
textSpan
.
id
=
`text-
${
pageNum
}
-
${
index
}
`
;
// 添加唯一ID便于调试
textSpan
.
style
.
position
=
'absolute'
;
// 使用百分比方法计算文字在turn.js页面中的位置
// 先计算PDF中的百分比位置,再乘以分页尺寸
// X坐标:计算PDF中的水平百分比
const
xPercent
=
item
.
transform
[
4
]
/
viewport
.
width
;
const
xPos
=
xPercent
*
turnPageWidth
;
// Y坐标:计算PDF中的垂直百分比(需要翻转)
// PDF中的Y坐标是文字框的顶部位置
// 我们需要考虑文字框的高度来调整位置
const
textHeight
=
item
.
height
;
const
pdfY
=
item
.
transform
[
5
];
// 方法1:如果PDF的Y是文字框顶部,我们需要调整到文字基线位置
// 文字基线通常在文字框高度的80%位置
const
baselineOffset
=
textHeight
*
0.8
;
// 文字基线偏移
const
adjustedPdfY
=
pdfY
+
baselineOffset
;
// 计算调整后的百分比位置
const
yPercent
=
1
-
(
adjustedPdfY
/
viewport
.
height
);
let
yPos
=
yPercent
*
turnPageHeight
;
// 调试百分比计算
if
(
index
<
3
)
{
console
.
log
(
`文字"
${
item
.
str
}
" 百分比计算:`
);
console
.
log
(
` PDF原始坐标: x=
${
item
.
transform
[
4
]}
, y=
${
item
.
transform
[
5
]}
`
);
console
.
log
(
` PDF原始Y:
${
pdfY
}
, 文字高度:
${
textHeight
}
`
);
console
.
log
(
` 基线偏移:
${
baselineOffset
.
toFixed
(
2
)}
(文字高度的80%)`
);
console
.
log
(
` 调整后PDF Y:
${
adjustedPdfY
.
toFixed
(
2
)}
`
);
console
.
log
(
` PDF百分比: x=
${(
xPercent
*
100
).
toFixed
(
2
)}
%, y=
${(
yPercent
*
100
).
toFixed
(
2
)}
%`
);
console
.
log
(
` turn.js尺寸:
${
turnPageWidth
}
x
${
turnPageHeight
}
`
);
console
.
log
(
` 最终位置: x=
${
xPos
.
toFixed
(
2
)}
, y=
${
yPos
.
toFixed
(
2
)}
`
);
console
.
log
(
` Y坐标说明: 考虑了文字框高度和基线偏移`
);
}
textSpan
.
style
.
left
=
`
${
xPos
}
px`
;
textSpan
.
style
.
top
=
`
${
yPos
}
px`
;
textSpan
.
style
.
fontSize
=
`
${
fontSize
}
px`
;
textSpan
.
style
.
fontFamily
=
fontFamily
;
textSpan
.
style
.
fontWeight
=
style
?.
fontWeight
||
'normal'
;
textSpan
.
style
.
fontStyle
=
style
?.
fontStyle
||
'normal'
;
// 设置文字尺寸
textSpan
.
style
.
width
=
`
${
item
.
width
*
baseScale
}
px`
;
textSpan
.
style
.
height
=
`
${
item
.
height
*
baseScale
}
px`
;
textSpan
.
style
.
lineHeight
=
`
${
item
.
height
*
baseScale
}
px`
;
// 文字颜色设为透明,但可以选中和复制
textSpan
.
style
.
color
=
'transparent'
;
textSpan
.
style
.
cursor
=
'text'
;
textSpan
.
style
.
whiteSpace
=
'pre'
;
textSpan
.
style
.
overflow
=
'hidden'
;
textSpan
.
style
.
backgroundColor
=
'transparent'
;
// 文字背景透明
// 添加选择事件支持
textSpan
.
addEventListener
(
'mouseup'
,
()
=>
{
const
selection
=
window
.
getSelection
();
if
(
selection
.
toString
())
{
console
.
log
(
'选中文本:'
,
selection
.
toString
());
}
});
textLayer
.
appendChild
(
textSpan
);
// 调试信息
if
(
index
<
5
)
{
// 只显示前5个文字项的信息
console
.
log
(
`文字项
${
index
}
: "
${
item
.
str
}
" 位置=(
${
xPos
.
toFixed
(
2
)}
,
${
yPos
.
toFixed
(
2
)}
) 尺寸=
${
item
.
width
*
baseScale
}
x
${
item
.
height
*
baseScale
}
`
);
console
.
log
(
` - 原始transform: [
${
item
.
transform
.
join
(
', '
)}
]`
);
console
.
log
(
` - 百分比位置: x=
${(
xPercent
*
100
).
toFixed
(
2
)}
%, y=
${(
yPercent
*
100
).
toFixed
(
2
)}
%`
);
}
});
// 将文字层添加到页面容器中
if
(
pageContainer
)
{
// 移除已存在的文字层
const
existingTextLayer
=
pageContainer
.
querySelector
(
'.text-layer'
);
if
(
existingTextLayer
)
{
existingTextLayer
.
remove
();
}
// 确保文字层在canvas之上
pageContainer
.
appendChild
(
textLayer
);
// 设置文字层的z-index,确保在canvas之上
textLayer
.
style
.
zIndex
=
'20'
;
}
console
.
log
(
`文字层渲染完成,共
${
textContent
.
items
.
length
}
个文字项`
);
console
.
log
(
`文字层位置: left=0, top=0, width=100%, height=100%`
);
console
.
log
(
`文字层z-index:
${
textLayer
.
style
.
zIndex
}
`
);
console
.
log
(
`文字层ID:
${
textLayer
.
id
}
`
);
// 验证文字层是否真的添加到DOM中
const
addedTextLayer
=
pageContainer
.
querySelector
(
`#text-layer-
${
pageNum
}
`
);
if
(
addedTextLayer
)
{
console
.
log
(
`✅ 文字层已成功添加到DOM,ID:
${
addedTextLayer
.
id
}
`
);
console
.
log
(
`文字层子元素数量:
${
addedTextLayer
.
children
.
length
}
`
);
// 最终验证文字层的实际尺寸
const
textLayerRect
=
addedTextLayer
.
getBoundingClientRect
();
const
pageContainerRect
=
pageContainer
.
getBoundingClientRect
();
console
.
log
(
`🔍 最终尺寸验证:`
);
console
.
log
(
` 文字层实际尺寸:
${
textLayerRect
.
width
.
toFixed
(
2
)}
x
${
textLayerRect
.
height
.
toFixed
(
2
)}
`
);
console
.
log
(
` 页面容器实际尺寸:
${
pageContainerRect
.
width
.
toFixed
(
2
)}
x
${
pageContainerRect
.
height
.
toFixed
(
2
)}
`
);
console
.
log
(
` 是否完全一致:
${
Math
.
abs
(
textLayerRect
.
width
-
pageContainerRect
.
width
)
<
1
&&
Math
.
abs
(
textLayerRect
.
height
-
pageContainerRect
.
height
)
<
1
?
'✅ 完全一致'
:
'❌ 有差异'
}
`
);
}
else
{
console
.
error
(
`❌ 文字层未能添加到DOM中!`
);
}
}
catch
(
textError
)
{
console
.
warn
(
`文字层渲染失败:`
,
textError
);
}
}
// 标记页面已渲染
if
(
processedPages
.
value
[
pageIndex
])
{
processedPages
.
value
[
pageIndex
].
isLoaded
=
true
;
}
// 检查canvas是否正确显示
const
renderedCanvas
=
pageContainer
.
querySelector
(
'canvas'
);
if
(
renderedCanvas
)
{
const
rect
=
renderedCanvas
.
getBoundingClientRect
();
console
.
log
(
`PDF页面
${
pageNum
}
渲染完成,canvas尺寸:
${
rect
.
width
}
x
${
rect
.
height
}
, 位置: (
${
rect
.
left
}
,
${
rect
.
top
}
), 可见性:
${
renderedCanvas
.
style
.
visibility
||
'visible'
}
, 显示:
${
renderedCanvas
.
style
.
display
||
'block'
}
`
);
}
else
{
console
.
warn
(
`PDF页面
${
pageNum
}
渲染完成,但找不到canvas元素`
);
}
}
catch
(
error
)
{
console
.
error
(
`渲染PDF页面
${
pageNum
}
失败:`
,
error
);
}
};
// 修改图片错误处理函数
const
handleImageError
=
async
(
index
)
=>
{
// 清除超时
...
...
@@ -722,6 +1289,84 @@ const toggleMute = () => {
localStorage
.
setItem
(
"bookReaderMuted"
,
isMuted
.
value
);
};
// 添加文字层开关函数
const
toggleTextLayer
=
()
=>
{
textLayerEnabled
.
value
=
!
textLayerEnabled
.
value
;
if
(
textLayerEnabled
.
value
)
{
// 清除渲染状态,重新渲染所有可见页面
renderedPages
.
value
.
clear
();
const
$magazine
=
$
(
magazine
.
value
);
if
(
$magazine
.
length
)
{
const
currentPage
=
$magazine
.
turn
(
"page"
);
renderVisiblePdfPages
(
currentPage
);
}
}
else
{
// 移除所有文字层
const
textLayers
=
document
.
querySelectorAll
(
'.text-layer'
);
textLayers
.
forEach
(
layer
=>
layer
.
remove
());
}
// 保存设置到本地存储
localStorage
.
setItem
(
"bookReaderTextLayer"
,
textLayerEnabled
.
value
);
};
// 添加清晰度切换函数
const
toggleHighQuality
=
()
=>
{
// 切换清晰度设置
const
newQuality
=
!
props
.
highQualityRendering
;
// 清除渲染状态,重新渲染所有可见页面
renderedPages
.
value
.
clear
();
// 重新渲染当前可见页面
const
$magazine
=
$
(
magazine
.
value
);
if
(
$magazine
.
length
)
{
const
currentPage
=
$magazine
.
turn
(
"page"
);
console
.
log
(
`切换清晰度设置:
${
newQuality
?
'高清晰度'
:
'标准清晰度'
}
`
);
renderVisiblePdfPages
(
currentPage
);
}
// 保存设置到本地存储
localStorage
.
setItem
(
"bookReaderHighQuality"
,
newQuality
);
};
// 添加调试函数:检查页面状态
const
debugPageStatus
=
()
=>
{
if
(
!
magazine
.
value
)
return
;
const
$magazine
=
$
(
magazine
.
value
);
const
currentPage
=
$magazine
.
turn
(
"page"
);
const
totalPages
=
$magazine
.
turn
(
"pages"
);
console
.
log
(
`=== 页面状态调试 ===`
);
console
.
log
(
`当前页码:
${
currentPage
}
, 总页数:
${
totalPages
}
`
);
console
.
log
(
`已渲染页面:`
,
Array
.
from
(
renderedPages
.
value
).
map
(
i
=>
i
+
1
));
// 检查当前页面的canvas状态
const
currentPageContainer
=
magazine
.
value
.
querySelector
(
`.p
${
currentPage
}
`
);
if
(
currentPageContainer
)
{
const
canvas
=
currentPageContainer
.
querySelector
(
'canvas'
);
if
(
canvas
)
{
const
rect
=
canvas
.
getBoundingClientRect
();
console
.
log
(
`当前页面canvas状态: 尺寸=
${
rect
.
width
}
x
${
rect
.
height
}
, 位置=(
${
rect
.
left
}
,
${
rect
.
top
}
), 可见性=
${
canvas
.
style
.
visibility
||
'visible'
}
, 显示=
${
canvas
.
style
.
display
||
'block'
}
`
);
}
else
{
console
.
log
(
`当前页面没有canvas元素`
);
}
}
// 检查所有页面的状态
for
(
let
i
=
1
;
i
<=
totalPages
;
i
++
)
{
const
pageContainer
=
magazine
.
value
.
querySelector
(
`.p
${
i
}
`
);
if
(
pageContainer
)
{
const
canvas
=
pageContainer
.
querySelector
(
'canvas'
);
const
isRendered
=
renderedPages
.
value
.
has
(
i
-
1
);
console
.
log
(
`页面
${
i
}
: 容器存在=
${
!!
pageContainer
}
, canvas存在=
${
!!
canvas
}
, 已渲染=
${
isRendered
}
`
);
}
}
console
.
log
(
`====================`
);
};
// 添加防抖处理
let
resizeTimeout
;
window
.
addEventListener
(
"resize"
,
()
=>
{
...
...
@@ -732,12 +1377,28 @@ window.addEventListener("resize", () => {
});
onMounted
(
async
()
=>
{
// 配置PDF.js worker
pdfjsLib
.
GlobalWorkerOptions
.
workerSrc
=
"/pdf.worker.min.mjs"
;
// 从本地存储读取静音状态
const
savedMuted
=
localStorage
.
getItem
(
"bookReaderMuted"
);
if
(
savedMuted
!==
null
)
{
isMuted
.
value
=
savedMuted
===
"true"
;
}
await
loadImages
();
// 从本地存储读取文字层设置
const
savedTextLayer
=
localStorage
.
getItem
(
"bookReaderTextLayer"
);
if
(
savedTextLayer
!==
null
)
{
textLayerEnabled
.
value
=
savedTextLayer
===
"true"
;
}
// 根据类型选择加载方式
if
(
props
.
isPdf
&&
props
.
pdfUrl
)
{
await
loadPdf
();
}
else
if
(
props
.
pages
&&
props
.
pages
.
length
>
0
)
{
await
loadImages
();
}
initAudio
();
window
.
addEventListener
(
"keydown"
,
handleKeyDown
);
window
.
addEventListener
(
"resize"
,
handleResize
);
...
...
@@ -754,6 +1415,8 @@ onUnmounted(() => {
imageCache
.
value
.
clear
();
loadingQueue
.
value
.
clear
();
preloadingPages
.
value
.
clear
();
renderedPages
.
value
.
clear
();
// 清除PDF渲染状态
pdfInitialized
.
value
=
false
;
// 清除PDF初始化状态
// 重置状态
initialLoading
.
value
=
true
;
...
...
@@ -917,6 +1580,81 @@ const showGuide = () => {
box-shadow
:
inset
0
0
20px
rgba
(
0
,
0
,
0
,
0
.05
);
}
/* PDF页面canvas样式 */
.page-canvas
{
width
:
100%
;
height
:
100%
;
display
:
block
;
background-color
:
white
;
/* 确保canvas在高分辨率下清晰显示 */
image-rendering
:
-
webkit-optimize-contrast
;
image-rendering
:
-
moz-crisp-edges
;
image-rendering
:
crisp-edges
;
image-rendering
:
pixelated
;
/* 防止浏览器自动缩放 */
image-rendering
:
auto
;
}
/* PDF文字层样式 */
.text-layer
{
position
:
absolute
;
left
:
0
;
top
:
0
;
right
:
0
;
bottom
:
0
;
overflow
:
hidden
;
opacity
:
0
.2
;
line-height
:
1
.0
;
pointer-events
:
auto
;
user-select
:
text
;
-webkit-user-select
:
text
;
-moz-user-select
:
text
;
-ms-user-select
:
text
;
z-index
:
10
;
}
.text-layer
>
span
{
color
:
transparent
;
position
:
absolute
;
white-space
:
pre
;
cursor
:
text
;
transform-origin
:
0%
0%
;
}
.text-layer
.highlight
{
margin
:
-1px
;
padding
:
1px
;
background-color
:
rgb
(
180
,
0
,
170
);
border-radius
:
4px
;
}
.text-layer
.highlight.begin
{
border-radius
:
4px
0px
0px
4px
;
}
.text-layer
.highlight.end
{
border-radius
:
0px
4px
4px
0px
;
}
.text-layer
.highlight.middle
{
border-radius
:
0px
;
}
.text-layer
.highlight.selected
{
background-color
:
rgb
(
0
,
100
,
0
);
}
/* 确保文字层在turn.js翻页时保持可见 */
.page
.text-layer
{
transform-style
:
preserve-3d
;
backface-visibility
:
visible
;
}
.page-canvas.wide-canvas
{
width
:
100%
;
height
:
100%
;
}
/* 缩放状态样式 */
.magazine.zoomed
{
cursor
:
move
;
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论