前端: Vue 3 + TypeScript + Vite
移动端: Android WebView + Kotlin/Java
PDF处理: PDF.js (Web端) + Android PDF库(可选)
文件传输: 分片上传/下载 + 本地缓存
src/
├── components/
│ ├── PDFViewer.vue # PDF预览组件
│ ├── PDFThumbnails.vue # 缩略图组件
│ └── PDFControls.vue # 控制面板
├── utils/
│ ├── pdfLoader.ts # PDF加载器
│ ├── cacheManager.ts # 缓存管理
│ └── webviewBridge.ts # WebView通信桥梁
├── views/
│ └── PDFPreview.vue # 预览页面
└── types/
└── pdf.ts # TypeScript类型定义
<!-- PDFViewer.vue -->
<template>
<div class="pdf-viewer-container">
<!-- 加载状态 -->
<div v-if="loading" class="loading-overlay">
<div class="progress-container">
<div class="progress-bar" :style="{ width: `${progress}%` }"></div>
<span>{{ progress }}%</span>
</div>
</div>
<!-- PDF容器 -->
<div ref="pdfContainer" class="pdf-container">
<canvas
v-for="page in visiblePages"
:key="page.pageNumber"
:ref="el => setCanvasRef(page.pageNumber, el)"
class="pdf-page"
/>
</div>
<!-- 控制面板 -->
<PDFControls
:current-page="currentPage"
:total-pages="totalPages"
:scale="scale"
@zoom-in="zoomIn"
@zoom-out="zoomOut"
@go-to-page="goToPage"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
import * as pdfjsLib from 'pdfjs-dist'
import { PDFLoader } from '../utils/pdfLoader'
import PDFControls from './PDFControls.vue'
// 初始化PDF.js
pdfjsLib.GlobalWorkerOptions.workerSrc =
'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'
interface Props {
fileUrl: string
fileSize?: number
}
const props = withDefaults(defineProps<Props>(), {
fileSize: 0
})
const pdfContainer = ref<HTMLElement>()
const pdfLoader = ref<PDFLoader>()
const loading = ref(false)
const progress = ref(0)
const totalPages = ref(0)
const currentPage = ref(1)
const scale = ref(1.5)
const visiblePages = ref<Array<{pageNumber: number}>>([])
const canvasRefs = ref<Map<number, HTMLCanvasElement>>(new Map())
// 分页配置
const PAGES_PER_VIEW = 3
const setCanvasRef = (pageNumber: number, el: HTMLCanvasElement | null) => {
if (el) {
canvasRefs.value.set(pageNumber, el)
}
}
// 初始化PDF加载器
const initPDFLoader = () => {
pdfLoader.value = new PDFLoader({
url: props.fileUrl,
fileSize: props.fileSize,
onProgress: (loaded, total) => {
progress.value = Math.round((loaded / total) * 100)
},
onLoad: async (pdfDocument) => {
totalPages.value = pdfDocument.numPages
await renderVisiblePages()
loading.value = false
},
onError: (error) => {
console.error('PDF加载失败:', error)
loading.value = false
}
})
}
// 渲染可见页面
const renderVisiblePages = async () => {
if (!pdfLoader.value) return
const startPage = Math.max(1, currentPage.value - 1)
const endPage = Math.min(totalPages.value, currentPage.value + PAGES_PER_VIEW - 1)
visiblePages.value = []
for (let i = startPage; i <= endPage; i++) {
visiblePages.value.push({ pageNumber: i })
}
// 批量渲染页面
await Promise.all(
visiblePages.value.map(async ({ pageNumber }) => {
await renderPage(pageNumber)
})
)
}
// 渲染单个页面
const renderPage = async (pageNumber: number) => {
if (!pdfLoader.value || !pdfLoader.value.pdfDocument) return
try {
const page = await pdfLoader.value.pdfDocument.getPage(pageNumber)
const canvas = canvasRefs.value.get(pageNumber)
if (!canvas) return
const viewport = page.getViewport({ scale: scale.value })
canvas.width = viewport.width
canvas.height = viewport.height
const context = canvas.getContext('2d')
if (!context) return
const renderContext = {
canvasContext: context,
viewport: viewport
}
await page.render(renderContext).promise
} catch (error) {
console.error(`渲染第${pageNumber}页失败:`, error)
}
}
// 控制方法
const zoomIn = () => {
scale.value = Math.min(scale.value + 0.25, 3)
renderVisiblePages()
}
const zoomOut = () => {
scale.value = Math.max(scale.value - 0.25, 0.5)
renderVisiblePages()
}
const goToPage = (page: number) => {
currentPage.value = Math.max(1, Math.min(page, totalPages.value))
renderVisiblePages()
}
// 生命周期
onMounted(async () => {
loading.value = true
initPDFLoader()
await pdfLoader.value?.load()
})
onUnmounted(() => {
pdfLoader.value?.cleanup()
})
// 监听文件URL变化
watch(() => props.fileUrl, () => {
if (pdfLoader.value) {
pdfLoader.value.cleanup()
}
initPDFLoader()
pdfLoader.value?.load()
})
</script>
<style scoped>
.pdf-viewer-container {
position: relative;
width: 100%;
height: 100%;
overflow: auto;
background: #f0f0f0;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.progress-container {
width: 80%;
background: #e0e0e0;
border-radius: 10px;
overflow: hidden;
position: relative;
}
.progress-bar {
height: 20px;
background: linear-gradient(90deg, #4CAF50, #8BC34A);
transition: width 0.3s ease;
}
.progress-container span {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: #333;
font-weight: bold;
}
.pdf-container {
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.pdf-page {
margin: 10px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
background: white;
}
</style>
// utils/pdfLoader.ts
import * as pdfjsLib from 'pdfjs-dist'
interface PDFLoaderOptions {
url: string
fileSize: number
onProgress?: (loaded: number, total: number) => void
onLoad?: (pdfDocument: pdfjsLib.PDFDocumentProxy) => void
onError?: (error: Error) => void
}
export class PDFLoader {
private pdfDocument: pdfjsLib.PDFDocumentProxy | null = null
private options: PDFLoaderOptions
private loadingTask: pdfjsLib.PDFDocumentLoadingTask | null = null
private chunkSize: number = 1024 * 1024 // 1MB分片
private loadedChunks: Uint8Array[] = []
private totalLoaded: number = 0
constructor(options: PDFLoaderOptions) {
this.options = options
}
// 分片加载PDF
async load(): Promise<void> {
try {
if (this.options.fileSize <= this.chunkSize * 4) {
// 小文件直接加载
await this.loadFullPDF()
} else {
// 大文件分片加载
await this.loadChunkedPDF()
}
} catch (error) {
this.options.onError?.(error as Error)
}
}
private async loadFullPDF(): Promise<void> {
this.loadingTask = pdfjsLib.getDocument({
url: this.options.url,
rangeChunkSize: this.chunkSize,
disableAutoFetch: true
})
this.loadingTask.onProgress = (progress) => {
this.options.onProgress?.(progress.loaded, progress.total)
}
this.pdfDocument = await this.loadingTask.promise
this.options.onLoad?.(this.pdfDocument)
}
private async loadChunkedPDF(): Promise<void> {
const totalChunks = Math.ceil(this.options.fileSize / this.chunkSize)
for (let i = 0; i < totalChunks; i++) {
const start = i * this.chunkSize
const end = Math.min(start + this.chunkSize, this.options.fileSize) - 1
const chunk = await this.fetchChunk(start, end)
this.loadedChunks.push(chunk)
this.totalLoaded += chunk.length
this.options.onProgress?.(this.totalLoaded, this.options.fileSize)
// 当加载到足够数据时开始解析PDF
if (i === 0 || this.totalLoaded > this.chunkSize * 2) {
await this.updatePDFDocument()
}
}
}
private async fetchChunk(start: number, end: number): Promise<Uint8Array> {
const response = await fetch(this.options.url, {
headers: {
'Range': `bytes=${start}-${end}`
}
})
if (!response.ok) {
throw new Error(`分片加载失败: ${response.status}`)
}
const arrayBuffer = await response.arrayBuffer()
return new Uint8Array(arrayBuffer)
}
private async updatePDFDocument(): Promise<void> {
// 合并已加载的分片
const totalLength = this.loadedChunks.reduce((sum, chunk) => sum + chunk.length, 0)
const mergedData = new Uint8Array(totalLength)
let offset = 0
for (const chunk of this.loadedChunks) {
mergedData.set(chunk, offset)
offset += chunk.length
}
// 创建PDF文档
if (!this.pdfDocument) {
this.loadingTask = pdfjsLib.getDocument({
data: mergedData,
rangeChunkSize: this.chunkSize,
disableAutoFetch: true
})
this.pdfDocument = await this.loadingTask.promise
this.options.onLoad?.(this.pdfDocument)
}
}
cleanup(): void {
if (this.pdfDocument) {
this.pdfDocument.destroy()
this.pdfDocument = null
}
if (this.loadingTask) {
this.loadingTask.destroy()
this.loadingTask = null
}
this.loadedChunks = []
this.totalLoaded = 0
}
}
// utils/webviewBridge.ts
interface BridgeMessage {
type: string
data: any
callbackId?: string
}
export class WebViewBridge {
private callbacks: Map<string, (data: any) => void> = new Map()
private messageQueue: BridgeMessage[] = []
constructor() {
this.setupMessageListener()
}
// 发送消息到Android
sendToAndroid(type: string, data?: any): Promise<any> {
return new Promise((resolve) => {
const callbackId = `callback_${Date.now()}_${Math.random()}`
this.callbacks.set(callbackId, (response) => {
resolve(response)
this.callbacks.delete(callbackId)
})
const message: BridgeMessage = {
type,
data,
callbackId
}
if (window.AndroidBridge) {
// 直接调用Android接口
window.AndroidBridge.postMessage(JSON.stringify(message))
} else {
// 使用自定义协议(fallback)
this.sendViaCustomScheme(message)
}
})
}
// 设置消息监听器
private setupMessageListener(): void {
window.addEventListener('message', (event) => {
try {
const message: BridgeMessage = typeof event.data === 'string'
? JSON.parse(event.data)
: event.data
if (message.callbackId && this.callbacks.has(message.callbackId)) {
const callback = this.callbacks.get(message.callbackId)!
callback(message.data)
}
} catch (error) {
console.error('解析消息失败:', error)
}
})
}
// 通过自定义协议发送(兼容性方案)
private sendViaCustomScheme(message: BridgeMessage): void {
const url = `androidbridge://${encodeURIComponent(JSON.stringify(message))}`
const iframe = document.createElement('iframe')
iframe.style.display = 'none'
iframe.src = url
document.body.appendChild(iframe)
setTimeout(() => {
document.body.removeChild(iframe)
}, 0)
}
// 请求本地文件
async requestLocalFile(filePath: string): Promise<string> {
return this.sendToAndroid('GET_LOCAL_FILE', { filePath })
}
// 获取设备信息
async getDeviceInfo(): Promise<DeviceInfo> {
return this.sendToAndroid('GET_DEVICE_INFO')
}
// 下载文件到本地
async downloadFile(url: string, fileName: string): Promise<string> {
return this.sendToAndroid('DOWNLOAD_FILE', { url, fileName })
}
// 检查文件缓存
async checkFileCache(fileUrl: string): Promise<CacheInfo> {
return this.sendToAndroid('CHECK_CACHE', { fileUrl })
}
}
// 全局声明
declare global {
interface Window {
AndroidBridge?: {
postMessage: (message: string) => void
}
}
}
export const bridge = new WebViewBridge()
// PDFWebViewActivity.kt
class PDFWebViewActivity : AppCompatActivity() {
private lateinit var webView: WebView
private lateinit var progressBar: ProgressBar
private var downloadManager: DownloadManager? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_pdf_webview)
webView = findViewById(R.id.webView)
progressBar = findViewById(R.id.progressBar)
setupWebView()
loadPDFViewer()
}
private fun setupWebView() {
// 启用JavaScript
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.setAppCacheEnabled(true)
webView.settings.cacheMode = WebSettings.LOAD_DEFAULT
// 设置WebChromeClient支持进度显示
webView.webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
progressBar.progress = newProgress
if (newProgress == 100) {
progressBar.visibility = View.GONE
}
}
}
// 设置WebViewClient拦截请求
webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest
): WebResourceResponse? {
val url = request.url.toString()
// 拦截PDF文件请求,使用本地缓存
if (url.endsWith(".pdf")) {
return handlePDFRequest(url, request)
}
return super.shouldInterceptRequest(view, request)
}
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
// 处理加载错误
handleLoadError(error)
}
}
// 添加JavaScript接口
webView.addJavascriptInterface(WebViewBridge(this), "AndroidBridge")
}
private fun handlePDFRequest(
url: String,
request: WebResourceRequest
): WebResourceResponse? {
return try {
// 检查本地缓存
val cachedFile = getCachedFile(url)
if (cachedFile != null && cachedFile.exists()) {
// 从缓存返回
val mimeType = "application/pdf"
val inputStream = FileInputStream(cachedFile)
WebResourceResponse(mimeType, "UTF-8", inputStream)
} else {
// 下载并缓存文件
downloadAndCachePDF(url)
null
}
} catch (e: Exception) {
Log.e("PDFWebView", "处理PDF请求失败", e)
null
}
}
private fun getCachedFile(url: String): File? {
val fileName = getFileNameFromUrl(url)
val cacheDir = getExternalFilesDir("pdf_cache")
return File(cacheDir, fileName)
}
private fun downloadAndCachePDF(url: String) {
// 使用DownloadManager或OkHttp下载文件
val request = DownloadManager.Request(Uri.parse(url))
.setTitle("下载PDF文件")
.setDescription("正在下载...")
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
.setDestinationInExternalFilesDir(this, "pdf_cache", getFileNameFromUrl(url))
downloadManager = getSystemService(DOWNLOAD_SERVICE) as DownloadManager
downloadManager?.enqueue(request)
}
private fun getFileNameFromUrl(url: String): String {
return try {
URL(url).path.substringAfterLast('/')
} catch (e: Exception) {
"document_${System.currentTimeMillis()}.pdf"
}
}
private fun loadPDFViewer() {
// 加载本地HTML或远程URL
webView.loadUrl("file:///android_asset/pdf_viewer.html")
// 或者加载远程地址
// webView.loadUrl("https://your-domain.com/pdf-viewer")
}
// WebView与JavaScript通信桥梁
inner class WebViewBridge(private val context: Context) {
@JavascriptInterface
fun postMessage(message: String) {
runOnUiThread {
handleJavaScriptMessage(message)
}
}
@JavascriptInterface
fun getDeviceInfo(): String {
val deviceInfo = mapOf(
"platform" to "Android",
"version" to Build.VERSION.RELEASE,
"model" to Build.MODEL,
"sdkVersion" to Build.VERSION.SDK_INT
)
return Gson().toJson(deviceInfo)
}
@JavascriptInterface
fun downloadFile(url: String, fileName: String): String {
return try {
val filePath = downloadFileToCache(url, fileName)
mapOf("success" to true, "filePath" to filePath).toJson()
} catch (e: Exception) {
mapOf("success" to false, "error" to e.message).toJson()
}
}
}
}
// PDFCacheManager.kt
class PDFCacheManager(private val context: Context) {
private val cacheDir: File by lazy {
File(context.externalCacheDir, "pdf_cache").apply {
if (!exists()) mkdirs()
}
}
// 分片下载大文件
suspend fun downloadLargeFile(
url: String,
fileName: String,
chunkSize: Long = 1024 * 1024 // 1MB
): File {
val outputFile = File(cacheDir, fileName)
return withContext(Dispatchers.IO) {
val connection = URL(url).openConnection() as HttpURLConnection
try {
val totalSize = connection.contentLengthLong
var downloaded: Long = 0
// 支持断点续传
if (outputFile.exists()) {
downloaded = outputFile.length()
connection.setRequestProperty("Range", "bytes=$downloaded-")
}
connection.connect()
val inputStream = connection.inputStream
val outputStream = FileOutputStream(outputFile, downloaded > 0)
val buffer = ByteArray(8192)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
downloaded += bytesRead
// 发送下载进度
sendProgress(downloaded, totalSize)
}
outputStream.close()
inputStream.close()
outputFile
} finally {
connection.disconnect()
}
}
}
// 文件分片读取
fun readFileChunks(file: File, chunkSize: Int = 1024 * 1024): Flow<ByteArray> {
return flow {
val buffer = ByteArray(chunkSize)
val inputStream = FileInputStream(file)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
val chunk = if (bytesRead == chunkSize) {
buffer
} else {
buffer.copyOf(bytesRead)
}
emit(chunk)
}
inputStream.close()
}.flowOn(Dispatchers.IO)
}
// 清理过期缓存
fun cleanCache(maxAgeDays: Int = 7) {
val cutoffTime = System.currentTimeMillis() - (maxAgeDays * 24 * 60 * 60 * 1000)
cacheDir.listFiles()?.forEach { file ->
if (file.lastModified() < cutoffTime) {
file.delete()
}
}
}
private fun sendProgress(downloaded: Long, total: Long) {
// 可以通过EventBus或回调发送进度
val progress = (downloaded.toFloat() / total * 100).toInt()
// 发送进度更新
}
}
// utils/performanceOptimizer.ts
export class PDFPerformanceOptimizer {
private static instance: PDFPerformanceOptimizer
private renderQueue: Map<number, Function> = new Map()
private isRendering = false
static getInstance(): PDFPerformanceOptimizer {
if (!PDFPerformanceOptimizer.instance) {
PDFPerformanceOptimizer.instance = new PDFPerformanceOptimizer()
}
return PDFPerformanceOptimizer.instance
}
// 防抖渲染
debouncedRender(pageNumber: number, renderFn: Function, delay = 100): void {
// 取消同页面的未执行渲染
if (this.renderQueue.has(pageNumber)) {
const oldFn = this.renderQueue.get(pageNumber)!
this.renderQueue.delete(pageNumber)
}
// 添加新的渲染任务
this.renderQueue.set(pageNumber, () => {
renderFn()
this.renderQueue.delete(pageNumber)
})
// 执行渲染队列
if (!this.isRendering) {
this.isRendering = true
setTimeout(() => {
this.executeRenderQueue()
this.isRendering = false
}, delay)
}
}
private executeRenderQueue(): void {
this.renderQueue.forEach(fn => fn())
this.renderQueue.clear()
}
// 图片懒加载
setupLazyLoading(container: HTMLElement): void {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const canvas = entry.target as HTMLCanvasElement
const pageNumber = parseInt(canvas.dataset.pageNumber || '1')
this.triggerRender(pageNumber)
observer.unobserve(canvas)
}
})
}, {
root: container,
threshold: 0.1
})
container.querySelectorAll('canvas[data-lazy]').forEach(canvas => {
observer.observe(canvas)
})
}
private triggerRender(pageNumber: number): void {
// 触发具体页面的渲染
}
// 内存管理
static cleanupMemory(canvasMap: Map<number, HTMLCanvasElement>): void {
canvasMap.forEach((canvas, pageNumber) => {
const context = canvas.getContext('2d')
context?.clearRect(0, 0, canvas.width, canvas.height)
canvas.width = 0
canvas.height = 0
})
canvasMap.clear()
}
}
// utils/cacheManager.ts
interface CacheConfig {
maxSize: number
maxAge: number
strategy: 'lru' | 'fifo'
}
export class PDFCacheManager {
private cache: Map<string, CacheItem> = new Map()
private config: CacheConfig
constructor(config?: Partial<CacheConfig>) {
this.config = {
maxSize: 100 * 1024 * 1024, // 100MB
maxAge: 24 * 60 * 60 * 1000, // 24小时
strategy: 'lru',
...config
}
}
async getOrLoad<T>(
key: string,
loader: () => Promise<T>,
sizeEstimator?: (data: T) => number
): Promise<T> {
// 检查缓存
const cached = this.cache.get(key)
if (cached && !this.isExpired(cached)) {
// 更新LRU位置
cached.lastAccessed = Date.now()
return cached.data as T
}
// 加载数据
const data = await loader()
const size = sizeEstimator?.(data) || 1
// 清理空间
this.makeSpace(size)
// 存入缓存
this.cache.set(key, {
data,
size,
createdAt: Date.now(),
lastAccessed: Date.now()
})
return data
}
private makeSpace(requiredSize: number): void {
let currentSize = Array.from(this.cache.values())
.reduce((sum, item) => sum + item.size, 0)
if (currentSize + requiredSize <= this.config.maxSize) {
return
}
// 根据策略清理缓存
const entries = Array.from(this.cache.entries())
if (this.config.strategy === 'lru') {
entries.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed)
} else {
entries.sort((a, b) => a[1].createdAt - b[1].createdAt)
}
for (const [key, item] of entries) {
if (currentSize + requiredSize <= this.config.maxSize) {
break
}
this.cache.delete(key)
currentSize -= item.size
}
}
private isExpired(item: CacheItem): boolean {
return Date.now() - item.createdAt > this.config.maxAge
}
}
interface CacheItem {
data: any
size: number
createdAt: number
lastAccessed: number
}
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
base: './', // 使用相对路径
build: {
outDir: 'dist',
assetsDir: 'assets',
rollupOptions: {
output: {
manualChunks: {
'pdfjs': ['pdfjs-dist'],
'vendor': ['vue', 'vue-router']
},
chunkFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]'
}
},
// 优化配置
chunkSizeWarningLimit: 1000,
cssCodeSplit: true,
sourcemap: false
},
server: {
proxy: {
'/api': {
target: 'http://your-backend.com',
changeOrigin: true
}
}
}
})
<!-- AndroidManifest.xml 部分配置 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:hardwareAccelerated="true"
android:largeHeap="true">
<activity
android:name=".PDFWebViewActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/pdf" />
</intent-filter>
</activity>
<!-- 文件提供者 -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
// utils/errorHandler.ts
export class PDFErrorHandler {
private static errors: ErrorLog[] = []
static handleError(error: Error, context?: string): void {
const errorLog: ErrorLog = {
message: error.message,
stack: error.stack,
context,
timestamp: Date.now(),
userAgent: navigator.userAgent
}
this.errors.push(errorLog)
// 发送到监控系统
this.reportToServer(errorLog)
// 用户友好提示
this.showUserMessage(error)
}
static handleWebViewError(errorCode: number, description: string): void {
const errorMap: Record<number, string> = {
400: '请求参数错误',
403: '访问被拒绝',
404: '文件不存在',
500: '服务器错误',
503: '服务不可用'
}
const userMessage = errorMap[errorCode] || '加载失败,请重试'
this.showUserMessage(new Error(userMessage))
}
private static showUserMessage(error: Error): void {
// 显示友好的错误提示
const message = this.getFriendlyMessage(error)
// 使用Toast或模态框显示
if (typeof window.AndroidBridge !== 'undefined') {
window.AndroidBridge.showToast?.(message)
} else {
alert(message)
}
}
private static getFriendlyMessage(error: Error): string {
const message = error.message.toLowerCase()
if (message.includes('network')) {
return '网络连接失败,请检查网络设置'
} else if (message.includes('timeout')) {
return '请求超时,请重试'
} else if (message.includes('format')) {
return '文件格式错误'
} else if (message.includes('permission')) {
return '没有文件访问权限'
} else {
return '加载失败,请重试'
}
}
private static reportToServer(errorLog: ErrorLog): void {
// 发送错误日志到服务器
fetch('/api/error-log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorLog)
}).catch(console.error)
}
}
interface ErrorLog {
message: string
stack?: string
context?: string
timestamp: number
userAgent: string
}
这个解决方案结合了Vue的前端渲染能力和Android WebView的本地功能,实现了:
大文件支持:通过分片加载技术处理GB级PDF文件 性能优化:懒加载、内存管理、缓存策略 完整功能:缩放、翻页、缩略图、搜索等 离线可用:完善的缓存机制 跨平台:可扩展为iOS和其他平台实际部署时,需要根据具体业务需求调整配置参数,并进行充分的测试。