Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Y
yd-middle-front
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
xingmin
yd-middle-front
Commits
c6c27c3d
Commit
c6c27c3d
authored
Nov 12, 2025
by
zhangxingmin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
push
parent
19f3caf2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
573 additions
and
1 deletions
+573
-1
src/views/system/lazyPdf/index.vue
+572
-0
src/views/system/project/index.vue
+1
-1
No files found.
src/views/system/lazyPdf/index.vue
0 → 100644
View file @
c6c27c3d
<
template
>
<div
class=
"pdf-viewer-container"
>
<!-- 控制面板 -->
<div
class=
"control-panel"
>
<button
@
click=
"loadPDF"
:disabled=
"loading"
>
加载PDF
</button>
<button
@
click=
"downloadAndFixPDF"
:disabled=
"loading"
>
下载并修复PDF
</button>
<button
@
click=
"testSmallChunk"
:disabled=
"loading"
>
测试小文件下载
</button>
<button
@
click=
"resetViewer"
:disabled=
"loading"
>
重置
</button>
<span
class=
"page-info"
v-if=
"totalPages > 0"
>
第
{{
currentPage
}}
页 / 共
{{
totalPages
}}
页
</span>
</div>
<!-- 加载状态 -->
<div
v-if=
"loading"
class=
"loading-container"
>
<div
class=
"progress-info"
>
<div
v-if=
"downloadProgress
<
100
"
>
下载进度:
{{
downloadProgress
}}
%
<div
class=
"progress-bar"
>
<div
class=
"progress-fill"
:style=
"
{ width: downloadProgress + '%' }">
</div>
</div>
<div>
已下载:
{{
loadedProgressText
}}
</div>
</div>
<div
v-else
>
PDF处理中...
</div>
</div>
</div>
<!-- 错误信息 -->
<div
v-if=
"error"
class=
"error-message"
>
{{
error
}}
</div>
<!-- PDF 渲染区域 -->
<div
ref=
"pdfContainer"
class=
"pdf-container"
:class=
"
{ hidden: loading }">
<iframe
v-if=
"pdfUrl"
:src=
"pdfUrl"
width=
"100%"
height=
"600px"
style=
"border: none;"
@
load=
"onIframeLoad"
@
error=
"onIframeError"
></iframe>
<div
v-else-if=
"!loading"
class=
"no-pdf-message"
>
暂无PDF显示
</div>
</div>
<!-- 调试信息 -->
<div
v-if=
"debugInfo"
class=
"debug-info"
>
{{
debugInfo
}}
</div>
<!-- 下载链接 -->
<div
v-if=
"pdfUrl"
style=
"margin-top: 10px;"
>
<a
:href=
"pdfUrl"
download=
"document.pdf"
style=
"color: green; font-weight: bold;"
>
↓ 下载PDF文件
</a>
</div>
</div>
</
template
>
<
script
>
import
axios
from
'axios'
;
export
default
{
name
:
'PDFViewer'
,
data
()
{
return
{
fileKey
:
'pdf/2025/10/11/company-intro.pdf'
,
currentPage
:
1
,
totalPages
:
0
,
loading
:
false
,
error
:
''
,
debugInfo
:
''
,
totalSize
:
0
,
loadedSize
:
0
,
pdfUrl
:
''
,
downloadProgress
:
0
,
// 配置项
chunkSize
:
2
*
1024
*
1024
,
// 2MB 每块
maxConcurrentDownloads
:
3
,
// 最大并发下载数
};
},
computed
:
{
loadedProgressText
()
{
if
(
this
.
totalSize
===
0
)
return
''
;
const
loadedMB
=
(
this
.
loadedSize
/
1024
/
1024
).
toFixed
(
1
);
const
totalMB
=
(
this
.
totalSize
/
1024
/
1024
).
toFixed
(
1
);
return
`
${
loadedMB
}
MB /
${
totalMB
}
MB`
;
}
},
methods
:
{
async
loadPDF
()
{
await
this
.
downloadAndRenderPDF
();
},
async
downloadAndFixPDF
()
{
await
this
.
downloadAndRenderPDF
(
true
);
},
async
downloadAndRenderPDF
(
attemptFix
=
false
)
{
this
.
loading
=
true
;
this
.
error
=
''
;
this
.
downloadProgress
=
0
;
this
.
pdfUrl
=
''
;
try
{
this
.
debugInfo
=
'开始加载PDF...'
;
// 获取文件大小
const
fileSize
=
await
this
.
getFileSize
();
this
.
totalSize
=
fileSize
;
this
.
debugInfo
=
`文件大小:
${(
fileSize
/
1024
/
1024
).
toFixed
(
1
)}
MB`
;
// 分块下载完整文件
const
fullPdfData
=
await
this
.
downloadFileByChunks
(
fileSize
);
// 检查并修复PDF
let
processedData
=
fullPdfData
;
if
(
attemptFix
)
{
processedData
=
await
this
.
attemptFixPDF
(
fullPdfData
);
}
// 使用 iframe 渲染
await
this
.
renderWithIframe
(
processedData
);
this
.
debugInfo
=
`PDF 加载完成,大小:
${(
processedData
.
byteLength
/
1024
/
1024
).
toFixed
(
1
)}
MB`
;
}
catch
(
error
)
{
this
.
error
=
`PDF 加载失败:
${
error
.
message
}
`
;
console
.
error
(
'PDF 加载失败:'
,
error
);
}
finally
{
this
.
loading
=
false
;
}
},
// 尝试修复PDF文件
async
attemptFixPDF
(
pdfData
)
{
this
.
debugInfo
=
'尝试修复PDF文件...'
;
const
dataView
=
new
DataView
(
pdfData
);
const
dataArray
=
new
Uint8Array
(
pdfData
);
// 检查文件头
const
header
=
String
.
fromCharCode
(...
dataArray
.
slice
(
0
,
8
));
console
.
log
(
'PDF文件头:'
,
header
);
if
(
header
.
substring
(
0
,
4
)
!==
'%PDF'
)
{
throw
new
Error
(
'无效的PDF文件头'
);
}
// 查找文件尾的xref表和trailer
const
trailerInfo
=
this
.
findTrailerAndXref
(
dataArray
);
console
.
log
(
'Trailer信息:'
,
trailerInfo
);
if
(
!
trailerInfo
.
foundTrailer
)
{
console
.
warn
(
'未找到完整的trailer,尝试手动添加EOF'
);
// 手动添加EOF标记
return
this
.
addManualEOF
(
dataArray
);
}
return
pdfData
;
},
// 查找trailer和xref表
findTrailerAndXref
(
dataArray
)
{
const
decoder
=
new
TextDecoder
(
'ascii'
);
const
dataString
=
decoder
.
decode
(
dataArray
);
// 从文件末尾开始查找
const
chunkSize
=
1024
;
let
foundTrailer
=
false
;
let
foundStartXref
=
false
;
let
xrefOffset
=
-
1
;
// 检查最后几个chunk
for
(
let
i
=
0
;
i
<
5
;
i
++
)
{
const
start
=
Math
.
max
(
0
,
dataArray
.
length
-
chunkSize
*
(
i
+
1
));
const
end
=
dataArray
.
length
-
chunkSize
*
i
;
const
chunk
=
dataArray
.
slice
(
start
,
end
);
const
chunkString
=
decoder
.
decode
(
chunk
);
if
(
chunkString
.
includes
(
'trailer'
))
{
foundTrailer
=
true
;
}
if
(
chunkString
.
includes
(
'startxref'
))
{
foundStartXref
=
true
;
// 提取xref偏移量
const
startxrefMatch
=
chunkString
.
match
(
/startxref
\s
+
(\d
+
)
/
);
if
(
startxrefMatch
)
{
xrefOffset
=
parseInt
(
startxrefMatch
[
1
]);
}
}
if
(
chunkString
.
includes
(
'%%EOF'
))
{
console
.
log
(
'找到EOF标记'
);
}
}
return
{
foundTrailer
,
foundStartXref
,
xrefOffset
};
},
// 手动添加EOF标记
addManualEOF
(
dataArray
)
{
this
.
debugInfo
=
'手动添加PDF结束标记...'
;
// 创建新的ArrayBuffer,额外空间用于添加EOF
const
newBuffer
=
new
ArrayBuffer
(
dataArray
.
length
+
50
);
const
newArray
=
new
Uint8Array
(
newBuffer
);
// 复制原始数据
newArray
.
set
(
dataArray
,
0
);
// 添加基本的PDF结束结构
const
eofString
=
'
\
n
\
n%%EOF
\
n'
;
const
encoder
=
new
TextEncoder
();
const
eofBytes
=
encoder
.
encode
(
eofString
);
newArray
.
set
(
eofBytes
,
dataArray
.
length
);
console
.
log
(
'已添加手动EOF标记'
);
return
newArray
.
buffer
;
},
// 测试小文件下载
async
testSmallChunk
()
{
this
.
loading
=
true
;
this
.
error
=
''
;
try
{
this
.
debugInfo
=
'测试小文件下载...'
;
// 只下载前5MB
const
testSize
=
5
*
1024
*
1024
;
const
testData
=
await
this
.
downloadChunk
(
0
,
testSize
-
1
);
// 检查下载的数据
await
this
.
analyzePDFChunk
(
testData
);
this
.
debugInfo
=
'小文件测试完成'
;
}
catch
(
error
)
{
this
.
error
=
`测试失败:
${
error
.
message
}
`
;
}
finally
{
this
.
loading
=
false
;
}
},
// 分析PDF数据块
async
analyzePDFChunk
(
pdfData
)
{
const
dataArray
=
new
Uint8Array
(
pdfData
);
const
decoder
=
new
TextDecoder
(
'ascii'
);
// 检查文件头
const
header
=
String
.
fromCharCode
(...
dataArray
.
slice
(
0
,
8
));
console
.
log
(
'测试文件头:'
,
header
);
// 检查文件内容
const
sampleSize
=
Math
.
min
(
1000
,
dataArray
.
length
);
const
sampleStart
=
decoder
.
decode
(
dataArray
.
slice
(
0
,
sampleSize
));
const
sampleEnd
=
decoder
.
decode
(
dataArray
.
slice
(
-
sampleSize
));
console
.
log
(
'文件开始样本:'
,
sampleStart
.
substring
(
0
,
200
));
console
.
log
(
'文件结束样本:'
,
sampleEnd
.
substring
(
Math
.
max
(
0
,
sampleEnd
.
length
-
200
)));
// 检查关键标记
const
hasObj
=
sampleStart
.
includes
(
'obj'
);
const
hasEndObj
=
sampleStart
.
includes
(
'endobj'
);
const
hasXref
=
sampleEnd
.
includes
(
'xref'
);
const
hasTrailer
=
sampleEnd
.
includes
(
'trailer'
);
const
hasEOF
=
sampleEnd
.
includes
(
'%%EOF'
);
console
.
log
(
'PDF结构检查:'
,
{
hasObj
,
hasEndObj
,
hasXref
,
hasTrailer
,
hasEOF
});
this
.
debugInfo
=
`PDF结构: 对象
${
hasObj
?
'✓'
:
'✗'
}
XREF
${
hasXref
?
'✓'
:
'✗'
}
Trailer
${
hasTrailer
?
'✓'
:
'✗'
}
EOF
${
hasEOF
?
'✓'
:
'✗'
}
`
;
},
// 分块下载文件
async
downloadFileByChunks
(
totalSize
)
{
const
chunks
=
[];
const
chunkCount
=
Math
.
ceil
(
totalSize
/
this
.
chunkSize
);
console
.
log
(
`开始分块下载,总大小:
${(
totalSize
/
1024
/
1024
).
toFixed
(
1
)}
MB, 块数:
${
chunkCount
}
`
);
// 创建所有下载任务
const
chunkPromises
=
[];
for
(
let
i
=
0
;
i
<
chunkCount
;
i
++
)
{
const
start
=
i
*
this
.
chunkSize
;
const
end
=
Math
.
min
(
start
+
this
.
chunkSize
-
1
,
totalSize
-
1
);
chunkPromises
.
push
(()
=>
this
.
downloadChunkWithProgress
(
start
,
end
,
i
,
chunkCount
));
}
// 并发下载(限制并发数)
for
(
let
i
=
0
;
i
<
chunkPromises
.
length
;
i
+=
this
.
maxConcurrentDownloads
)
{
const
batch
=
chunkPromises
.
slice
(
i
,
i
+
this
.
maxConcurrentDownloads
);
const
batchResults
=
await
Promise
.
all
(
batch
.
map
(
fn
=>
fn
()));
chunks
.
push
(...
batchResults
);
console
.
log
(
`已完成
${
Math
.
min
(
i
+
this
.
maxConcurrentDownloads
,
chunkCount
)}
/
${
chunkCount
}
块下载`
);
}
// 合并所有块
return
this
.
mergeChunks
(
chunks
);
},
// 下载单个块(带进度)
async
downloadChunkWithProgress
(
start
,
end
,
chunkIndex
,
totalChunks
)
{
try
{
const
response
=
await
axios
.
get
(
'http://10.0.10.215:9106/oss/api/api/file/chunk'
,
{
params
:
{
fileKey
:
this
.
fileKey
,
start
:
start
,
end
:
end
},
responseType
:
'arraybuffer'
,
onDownloadProgress
:
(
progressEvent
)
=>
{
// 计算整体下载进度
const
chunkProgress
=
progressEvent
.
loaded
?
(
progressEvent
.
loaded
/
(
end
-
start
+
1
))
*
100
:
0
;
const
overallProgress
=
(
chunkIndex
/
totalChunks
)
*
100
+
(
chunkProgress
/
totalChunks
);
this
.
downloadProgress
=
Math
.
min
(
100
,
Math
.
round
(
overallProgress
));
}
});
this
.
loadedSize
=
Math
.
max
(
this
.
loadedSize
,
end
+
1
);
return
response
.
data
;
}
catch
(
error
)
{
console
.
error
(
`下载块
${
start
}
-
${
end
}
失败:`
,
error
);
throw
error
;
}
},
// 下载单个块(无进度)
async
downloadChunk
(
start
,
end
)
{
try
{
const
response
=
await
axios
.
get
(
'http://10.0.10.215:9106/oss/api/api/file/chunk'
,
{
params
:
{
fileKey
:
this
.
fileKey
,
start
:
start
,
end
:
end
},
responseType
:
'arraybuffer'
});
return
response
.
data
;
}
catch
(
error
)
{
console
.
error
(
`下载块
${
start
}
-
${
end
}
失败:`
,
error
);
throw
error
;
}
},
// 合并所有块
mergeChunks
(
chunks
)
{
const
totalLength
=
chunks
.
reduce
((
total
,
chunk
)
=>
total
+
chunk
.
byteLength
,
0
);
const
result
=
new
Uint8Array
(
totalLength
);
let
offset
=
0
;
chunks
.
forEach
(
chunk
=>
{
const
chunkArray
=
new
Uint8Array
(
chunk
);
result
.
set
(
chunkArray
,
offset
);
offset
+=
chunkArray
.
length
;
});
console
.
log
(
'合并完成,总大小:'
,
totalLength
);
return
result
;
},
// 使用 iframe 渲染
async
renderWithIframe
(
pdfData
)
{
try
{
this
.
debugInfo
=
'使用 iframe 渲染 PDF...'
;
// 创建 Blob 并生成 URL
const
blob
=
new
Blob
([
pdfData
],
{
type
:
'application/pdf'
});
this
.
pdfUrl
=
URL
.
createObjectURL
(
blob
);
this
.
debugInfo
=
'iframe PDF 渲染完成'
;
this
.
currentPage
=
1
;
this
.
totalPages
=
1
;
// iframe 模式下我们不知道总页数
}
catch
(
error
)
{
console
.
error
(
'iframe 渲染失败:'
,
error
);
throw
error
;
}
},
// iframe 加载成功
onIframeLoad
()
{
console
.
log
(
'iframe PDF 加载成功'
);
this
.
debugInfo
+=
' | iframe 加载成功'
;
},
// iframe 加载失败
onIframeError
()
{
console
.
error
(
'iframe PDF 加载失败'
);
this
.
error
=
'iframe 无法加载 PDF 文档'
;
},
// 获取文件大小
async
getFileSize
()
{
try
{
const
response
=
await
axios
.
get
(
'http://10.0.10.215:9106/oss/api/api/file/chunk'
,
{
params
:
{
fileKey
:
this
.
fileKey
,
start
:
0
,
end
:
1024
},
responseType
:
'arraybuffer'
});
// 尝试从响应头获取文件大小
const
contentRange
=
response
.
headers
[
'content-range'
];
if
(
contentRange
)
{
const
match
=
contentRange
.
match
(
/
\/(\d
+
)
$/
);
if
(
match
)
{
return
parseInt
(
match
[
1
]);
}
}
// 使用默认值
return
85
*
1024
*
1024
;
}
catch
(
error
)
{
console
.
error
(
'获取文件大小失败,使用默认值'
);
return
85
*
1024
*
1024
;
}
},
// 重置查看器
resetViewer
()
{
if
(
this
.
pdfUrl
)
{
URL
.
revokeObjectURL
(
this
.
pdfUrl
);
}
this
.
currentPage
=
1
;
this
.
totalPages
=
0
;
this
.
error
=
''
;
this
.
debugInfo
=
''
;
this
.
loadedSize
=
0
;
this
.
totalSize
=
0
;
this
.
pdfUrl
=
''
;
this
.
downloadProgress
=
0
;
}
},
beforeUnmount
()
{
this
.
resetViewer
();
}
};
</
script
>
<
style
scoped
>
.pdf-viewer-container
{
font-family
:
Arial
,
sans-serif
;
max-width
:
1200px
;
margin
:
0
auto
;
padding
:
20px
;
}
.control-panel
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
margin-bottom
:
20px
;
padding
:
15px
;
background
:
#f5f5f5
;
border-radius
:
8px
;
flex-wrap
:
wrap
;
}
.control-panel
button
{
padding
:
8px
16px
;
border
:
1px
solid
#ddd
;
background
:
white
;
border-radius
:
4px
;
cursor
:
pointer
;
transition
:
all
0.2s
;
}
.control-panel
button
:hover:not
(
:disabled
)
{
background
:
#e9e9e9
;
}
.control-panel
button
:disabled
{
opacity
:
0.6
;
cursor
:
not-allowed
;
}
.page-info
{
margin-left
:
auto
;
font-weight
:
bold
;
color
:
#333
;
}
.loading-container
{
text-align
:
center
;
padding
:
40px
;
}
.progress-info
{
max-width
:
400px
;
margin
:
0
auto
;
}
.progress-bar
{
width
:
100%
;
height
:
20px
;
background
:
#e0e0e0
;
border-radius
:
10px
;
margin-top
:
10px
;
overflow
:
hidden
;
}
.progress-fill
{
height
:
100%
;
background
:
#4CAF50
;
transition
:
width
0.3s
ease
;
}
.error-message
{
background
:
#ffebee
;
color
:
#c62828
;
padding
:
15px
;
border-radius
:
4px
;
margin
:
20px
0
;
border-left
:
4px
solid
#c62828
;
}
.pdf-container
{
border
:
1px
solid
#ddd
;
border-radius
:
8px
;
padding
:
20px
;
background
:
white
;
min-height
:
600px
;
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
}
.pdf-container.hidden
{
display
:
none
;
}
.no-pdf-message
{
color
:
#666
;
font-style
:
italic
;
text-align
:
center
;
margin-top
:
50px
;
}
.debug-info
{
margin-top
:
20px
;
padding
:
10px
;
background
:
#e3f2fd
;
border-radius
:
4px
;
font-size
:
12px
;
color
:
#1565c0
;
}
</
style
>
\ No newline at end of file
src/views/system/project/index.vue
View file @
c6c27c3d
...
...
@@ -278,7 +278,7 @@
</el-radio-group>
</el-form-item>
<el-form-item
label=
"项目图标"
label=
"项目图标"
0
prop=
"logoUrl"
:rules=
"[{ required: true, message: '请上传项目图标', trigger: 'change' }]"
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment