Commit c9fc0671 by Sweet Zhang

解决文件在手机上不能直接打开的问题

parent 1e4cf0b9
......@@ -149,6 +149,7 @@
uni.setStorageSync('isLogin','1');
uni.setStorageSync('loginType','codelogin');
uni.setStorageSync('cffp_userId', this.userId);
console.log('============',uni.getStorageSync('cffp_userId'))
uni.setStorageSync('uni-token', res.data['token']);
this.loginTypeSync = "codelogin";
this.queryInfo()
......
......@@ -182,6 +182,7 @@
this.userId = String(res['data']['userId']);
uni.setStorageSync('isLogin','1');
uni.setStorageSync('cffp_userId',this.userId);
console.log('============',uni.getStorageSync('cffp_userId'))
uni.setStorageSync('loginType',this.loginType);
uni.setStorageSync('uni-token', res.data['token']);
......
<!-- components/pdf-viewer/PdfViewer.vue -->
<template>
<view class="pdf-viewer" ref="pdfContainerRef" >
<!-- 横屏提示组件 -->
<LandscapeTip
:debug="false"
:auto-show="pdfInfo.landscapeFlag ? pdfInfo.landscapeFlag : false"
:show-delay="1000"
:check-wide-content="false"
/>
<!-- 添加一个滚动容器 -->
<view class="pdf-viewer" :style="{ height: containerHeight + 'px' }">
<!-- 标题 -->
<view v-if="pdfInfo.title" class="pdf-header">
<text class="pdf-title">{{ pdfInfo.title }}</text>
</view>
<!-- 滚动容器 -->
<scroll-view
class="pdf-scroll-view"
class="pdf-scroll"
scroll-y
:show-scrollbar="false"
@scroll="handleScroll"
@scroll="onScroll"
:scroll-top="scrollTop"
:style="{ height: scrollContainerHeight + 'px' }"
>
<!-- PDF文档信息 -->
<view class="pdf-info" v-if="pdfInfo.title">
<text class="pdf-title">{{ pdfInfo.title }}</text>
<text class="pdf-page-count" v-if="pdfPageCount > 0">{{ pdfPageCount }}</text>
</view>
<!-- 页面列表 -->
<view
v-for="pageIndex in pdfPageCount"
:key="pageIndex"
class="page-container"
:id="`page-${pageIndex}`"
v-for="pageNum in totalPages"
:key="pageNum"
class="page-item"
:style="{ minHeight: getPageMinHeight(pageNum) + 'px' }"
>
<view class="page-header" v-if="loadingStatus">
<text class="page-number">{{ pageIndex }}</text>
<text class="page-status" v-if="isPageLoading(pageIndex)">加载中...</text>
<text class="page-status error" v-else-if="isPageFailed(pageIndex)">加载失败</text>
<text class="page-status success" v-else-if="getPageImage(pageIndex)">加载完成</text>
<!-- 页码标签 -->
<view v-if="props.showPageNumber" class="page-number-tag">
{{ pageNum }}
</view>
<view class="page-content">
<view class="loadEffect" v-if="!getPageImage(pageIndex) || isPageLoading(pageIndex)"></view>
<!-- 缩小模式:widthFix + 固定最大高度防过长 -->
<view v-if="!isZoomed[pageNum] && hasImage(pageNum)" class="fit-mode">
<image
v-if="getPageImage(pageIndex)"
:src="getPageImage(pageIndex)"
:src="getImage(pageNum)"
mode="widthFix"
class="pdf-image"
@load="handlePageImageLoad(pageIndex)"
@error="handlePageImageError(pageIndex)"
class="pdf-image-fit"
:show-menu-by-longpress="false"
></image>
<view v-else-if="isPageFailed(pageIndex)" class="page-error" @click="retryLoadPage(pageIndex)">
<text class="error-text">页面加载失败,点击重试</text>
<text class="retry-count">已重试 {{ getPageRetryCount(pageIndex) }}</text>
</view>
<view v-else class="page-placeholder">
<text>页面加载中...</text>
/>
</view>
<!-- 高清模式:原始尺寸 + 可拖动 -->
<view
v-else-if="isZoomed[pageNum] && hasImage(pageNum)"
class="zoom-container"
@touchstart="onTouchStart($event, pageNum)"
@touchmove="onTouchMove($event, pageNum)"
@touchend="onTouchEnd"
@touchcancel="onTouchEnd"
>
<view
class="image-original"
:style="{
transform: `translate(${translateX[pageNum] || 0}px, ${translateY[pageNum] || 0}px)`,
width: (pageRenderWidth[pageNum] || 0) + 'px',
height: (pageRenderHeight[pageNum] || 0) + 'px'
}"
>
<image
:src="getImage(pageNum)"
mode="scaleToFill"
style="display: block; width: 100%; height: 100%;"
/>
</view>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading-state">
<view class="loading-spinner"></view>
<text>文件较大,正在加载中...</text>
<!-- 加载中 / 错误 -->
<view v-else-if="isLoading(pageNum)" class="placeholder loading">
<view class="spinner"></view>
</view>
<view
v-else-if="isFailed(pageNum)"
class="placeholder error"
@click="retryPage(pageNum)"
>
❌ 加载失败,点击重试
</view>
<!-- 错误状态 -->
<view v-if="error" class="error-state">
<text class="error-icon"></text>
<text class="error-message">{{ errorMessage }}</text>
<button class="retry-button" @click="initPdf">重试</button>
<!-- 操作按钮 -->
<view class="action-btns" v-if="hasImage(pageNum)">
<button
v-if="!isZoomed[pageNum]"
class="zoom-btn"
@click="toggleZoom(pageNum)"
>
放大查看
</button>
<button
v-else
class="reset-btn-inline"
@click="resetZoom(pageNum)"
>
重置
</button>
</view>
</view>
<!-- 加载进度 -->
<view v-if="!loading && !error && pdfPageCount > 0" class="progress-info">
<text>已加载 {{ loadedPages }}/{{ pdfPageCount }}</text>
<view class="progress-bar">
<view class="progress-inner" :style="{ width: `${(loadedPages / pdfPageCount) * 100}%` }"></view>
<!-- 全局状态 -->
<view v-if="globalLoading" class="global-status">
<view class="spinner"></view>
<text>正在加载文档...</text>
</view>
<view v-else-if="globalError" class="global-status error">
<text>{{ errorMessage }}</text>
<button size="mini" @click="reload">重试</button>
</view>
<view v-else-if="totalPages > 0" class="progress-bar">
<text>已加载 {{ loadedSet.size }} / {{ totalPages }}</text>
<view class="bar-bg">
<view class="bar-fill" :style="{ width: progress + '%' }"></view>
</view>
</view>
</scroll-view>
......@@ -82,778 +114,506 @@
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
// 导入本地安装的PDF.js
import * as pdfjsLib from 'pdfjs-dist';
// ================== IMPORTS ==================
import { ref, computed, onMounted, onUnmounted } from 'vue';
import * as pdfjsLib from 'pdfjs-dist/build/pdf';
// 👇 关键:静态导入 worker(Vite 语法)
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?url';
import LandscapeTip from '@/components/LandscapeTip/LandscapeTip.vue';
// ========================== 类型定义 ==========================
// ================== PROPS ==================
interface PdfInfo {
title?: string;
url: string;
landscapeFlag?:boolean;
}
interface Props {
const props = withDefaults(defineProps<{
pdfInfo: PdfInfo;
autoLoad?: boolean;
lazyLoad?: boolean;
maxRetryCount?: number;
loadingStatus?:boolean;
}
// ========================== Props & Emits ==========================
const props = withDefaults(defineProps<Props>(), {
showPageName?: boolean;
showPageNumber?: boolean;
}>(), {
autoLoad: true,
lazyLoad: true,
maxRetryCount: 3,
loadingStatus:false,
showPageNumber: false,
});
const emit = defineEmits<{
loadStart: [url: string];
loadComplete: [url: string, pageCount: number];
loadError: [url: string, error: Error];
pageChange: [currentPage: number, totalPages: number];
}>();
// ========================== 响应式数据 ==========================
const pdfImages = ref<string[]>([]);
const imgLoading = ref<boolean[]>([]);
const pdfPageCount = ref(0);
const currentLoading = ref(0);
const pdfDoc = ref<any>(null);
const failedPages = ref<Record<number, number>>({});
const loadingQueue = ref<number[]>([]);
const isProcessingQueue = ref(false);
const loading = ref(false);
const error = ref(false);
const errorMessage = ref('');
const currentPage = ref(1);
const lastScrollTime = ref(0);
const scrollThrottle = ref(300);
const loadedPageSet = ref<Set<number>>(new Set()); // 记录已加载的页面
// 添加 scrollTop 用于控制滚动位置
// ================== STATE ==================
const isMounted = ref(true);
const containerHeight = ref(0);
const scrollContainerHeight = ref(0);
// PDF 文档
const pdfDoc = ref<pdfjsLib.PDFDocumentProxy | null>(null);
const totalPages = ref(0);
// 页面数据(按页存储)
const images = ref<string[]>([]);
const loading = ref<boolean[]>([]);
const failed = ref<Record<number, number>>({});
const loadedSet = ref<Set<number>>(new Set());
// 👇 每页独立尺寸(支持横版/竖版)
const pageOriginalWidth = ref<Record<number, number>>({});
const pageOriginalHeight = ref<Record<number, number>>({});
const pageRenderWidth = ref<Record<number, number>>({});
const pageRenderHeight = ref<Record<number, number>>({});
// 双模式状态
const isZoomed = ref<Record<number, boolean>>({});
const translateX = ref<Record<number, number>>({});
const translateY = ref<Record<number, number>>({});
// 滚动 & 加载队列
const scrollTop = ref(0);
// ========================== 初始化PDF.js ==========================
// 设置worker
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;
// ========================== 横屏提示处理 ==========================
const autoLandscapeTipRef = ref();
/**
* 提示关闭回调
*/
const onTipClose = () => {
console.log('横屏提示已关闭');
};
/**
* 提示显示回调
*/
const onTipShow = () => {
console.log('横屏提示已显示');
};
const lastScroll = ref(0);
const queue = ref<number[]>([]);
const isProcessingQueue = ref(false);
/**
* 方向变化回调
*/
const onOrientationChange = (orientation: 'portrait' | 'landscape') => {
console.log('屏幕方向变为:', orientation);
};
// 全局状态
const globalLoading = ref(false);
const globalError = ref(false);
const errorMessage = ref('');
// ================== COMPUTED ==================
const progress = computed(() =>
totalPages.value > 0 ? (loadedSet.value.size / totalPages.value) * 100 : 0
);
// ========================== 计算属性 ==========================
const loadedPages = computed(() => {
return loadedPageSet.value.size;
});
const hasImage = (pageNum: number) => !!images.value[pageNum - 1];
const getImage = (pageNum: number) => images.value[pageNum - 1] || '';
const isLoading = (pageNum: number) => !!loading.value[pageNum - 1];
const isFailed = (pageNum: number) => (failed.value[pageNum] || 0) > 0;
const hasMoreToLoad = computed(() => {
return loadedPages.value < pdfPageCount.value;
});
// ========================== 生命周期 ==========================
// ================== LIFECYCLE ==================
onMounted(() => {
if (props.autoLoad) {
initPdf();
}
const sys = uni.getSystemInfoSync();
containerHeight.value = sys.windowHeight;
scrollContainerHeight.value = sys.windowHeight - (props.pdfInfo.title ? 80 : 40);
if (props.autoLoad) init();
});
onUnmounted(() => {
isMounted.value = false;
cleanup();
});
// ========================== 监听器 ==========================
watch(() => props.pdfInfo.url, (newUrl, oldUrl) => {
if (newUrl && newUrl !== oldUrl) {
resetState();
initPdf();
}
});
// ========================== 公共方法 ==========================
/**
* 初始化PDF
*/
const initPdf = async () => {
if (!props.pdfInfo.url) {
setError('PDF URL不能为空');
return;
}
// ================== INIT ==================
const init = async () => {
if (!props.pdfInfo.url) return setError('PDF URL 为空');
try {
resetState();
loading.value = true;
error.value = false;
emit('loadStart', props.pdfInfo.url);
await loadPdfDocument();
// 初始加载前3页
const initialPages = Math.min(3, pdfPageCount.value);
console.log(`初始加载前 ${initialPages} 页`);
for (let i = 1; i <= initialPages; i++) {
addToLoadingQueue(i);
}
processLoadingQueue();
// 延迟检查其他可见页面
nextTick(() => {
setTimeout(() => {
checkVisiblePages();
}, 800);
});
reset();
globalLoading.value = true;
await loadDocument();
preloadInitialPages();
} catch (err: any) {
setError('文件读取失败')
// setError(`PDF初始化失败: ${err.message}`);
emit('loadError', props.pdfInfo.url, err);
setError(err.message || '加载失败');
} finally {
loading.value = false;
globalLoading.value = false;
}
};
/**
* 重新加载PDF
*/
const reload = () => {
initPdf();
};
// ========================== 内部方法 ==========================
/**
* 重置状态
*/
const resetState = () => {
pdfImages.value = [];
imgLoading.value = [];
pdfPageCount.value = 0;
currentLoading.value = 0;
failedPages.value = {};
loadingQueue.value = [];
isProcessingQueue.value = false;
error.value = false;
errorMessage.value = '';
currentPage.value = 1;
loadedPageSet.value.clear();
// ================== LOAD DOCUMENT ==================
const loadDocument = async () => {
// ====== 平台差异化设置 worker ======
let useWorkerFlag = false;
if (pdfDoc.value) {
pdfDoc.value.destroy();
pdfDoc.value = null;
}
};
/**
* 清理资源
*/
const cleanup = () => {
if (pdfDoc.value) {
pdfDoc.value.destroy();
pdfDoc.value = null;
}
};
// #ifdef H5
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker; // ← 静态导入
useWorkerFlag = true;
// #endif
/**
* 设置错误状态
*/
const setError = (message: string) => {
error.value = true;
errorMessage.value = message;
loading.value = false;
};
// #ifndef H5
pdfjsLib.GlobalWorkerOptions.workerSrc = undefined;
useWorkerFlag = false;
// #endif
/**
* 加载PDF文档
*/
const loadPdfDocument = async (): Promise<number> => {
try {
const loadingTask = pdfjsLib.getDocument({
const doc = await pdfjsLib.getDocument({
url: props.pdfInfo.url,
cMapUrl: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.11.338/cmaps/',
cMapPacked: true,
disableFontFace: true,
useSystemFonts: true,
isEvalSupported: false,
});
useWorker: useWorkerFlag,
}).promise;
pdfDoc.value = await loadingTask.promise;
pdfPageCount.value = pdfDoc.value.numPages;
pdfDoc.value = doc;
totalPages.value = doc.numPages;
// 初始化数组
pdfImages.value = new Array(pdfPageCount.value).fill('');
imgLoading.value = new Array(pdfPageCount.value).fill(false);
emit('loadComplete', props.pdfInfo.url, pdfPageCount.value);
console.log(`PDF文档加载完成: ${props.pdfInfo.url}, 共 ${pdfPageCount.value} 页`);
return pdfPageCount.value;
images.value = new Array(doc.numPages).fill('');
loading.value = new Array(doc.numPages).fill(false);
};
} catch (err: any) {
console.error('PDF文档加载失败:', err);
throw new Error(`文档加载失败: ${err.message}`);
}
// ================== PAGE LOADING ==================
const preloadInitialPages = () => {
const count = Math.min(3, totalPages.value);
for (let i = 1; i <= count; i++) addToQueue(i);
processQueue();
};
const addToQueue = (pageNum: number) => {
if (!queue.value.includes(pageNum)) queue.value.push(pageNum);
};
/**
* 滚动处理 - 使用 scroll-view 的 scroll 事件
*/
const handleScroll = (e: any) => {
if (!props.lazyLoad || loading.value) return;
const processQueue = async () => {
if (isProcessingQueue.value || queue.value.length === 0) return;
isProcessingQueue.value = true;
const now = Date.now();
if (now - lastScrollTime.value < scrollThrottle.value) {
return;
while (queue.value.length > 0) {
const pageNum = queue.value.shift()!;
await loadPage(pageNum);
await new Promise(r => setTimeout(r, 10));
}
lastScrollTime.value = now;
// 使用防抖
clearTimeout((window as any).scrollTimer);
(window as any).scrollTimer = setTimeout(() => {
checkVisiblePages(e.detail.scrollTop);
}, 100);
isProcessingQueue.value = false;
};
/**
* 检查可见页面 - 修改为接收 scrollTop 参数
*/
const checkVisiblePages = (scrollTop: number) => {
if (pdfPageCount.value === 0 || loadingQueue.value.length > 5) return;
console.log('开始检查可见页面...', scrollTop);
const loadPage = async (pageNum: number) => {
if (!isMounted.value || !pdfDoc.value || loadedSet.value.has(pageNum)) return;
if (getRetryCount(pageNum) >= props.maxRetryCount) return;
const windowHeight = uni.getSystemInfoSync().windowHeight;
try {
loading.value[pageNum - 1] = true;
const page = await pdfDoc.value.getPage(pageNum);
// 计算可见区域
const visibleTop = scrollTop - 500; // 提前500px开始加载
const visibleBottom = scrollTop + windowHeight + 1000; // 延后1000px加载
// 获取原始尺寸
const originalViewport = page.getViewport({ scale: 1 });
pageOriginalWidth.value[pageNum] = originalViewport.width;
pageOriginalHeight.value[pageNum] = originalViewport.height;
// 检查每个页面是否在可见区域内
for (let i = 1; i <= pdfPageCount.value; i++) {
// 如果页面已经加载或正在加载,跳过
if (loadedPageSet.value.has(i) || isPageLoading(i)) {
continue;
}
// 计算高清渲染尺寸(目标宽度 ~1600px)
const targetPhysicalWidth = 1600;
const scale = Math.min(5.0, Math.max(1.0, targetPhysicalWidth / originalViewport.width));
const renderViewport = page.getViewport({ scale });
// 检查页面位置
uni.createSelectorQuery()
.select(`#page-${i}`)
.boundingClientRect((rect: any) => {
if (rect) {
const pageTop = scrollTop + rect.top;
const pageBottom = scrollTop + rect.bottom;
// 如果页面在可见区域内
if (pageBottom > visibleTop && pageTop < visibleBottom) {
console.log(`页面 ${i} 在可见区域内,准备加载`);
addToLoadingQueue(i);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.width = renderViewport.width;
canvas.height = renderViewport.height;
await page.render({ canvasContext: ctx, viewport: renderViewport }).promise;
// 存储渲染结果
pageRenderWidth.value[pageNum] = canvas.width;
pageRenderHeight.value[pageNum] = canvas.height;
images.value[pageNum - 1] = canvas.toDataURL('image/jpeg', 0.95);
loadedSet.value.add(pageNum);
delete failed.value[pageNum];
canvas.width = canvas.height = 0;
} catch (err) {
console.error(`Page ${pageNum} error:`, err);
failed.value[pageNum] = (failed.value[pageNum] || 0) + 1;
} finally {
if (isMounted.value) loading.value[pageNum - 1] = false;
}
};
// 更新当前页
if (rect.top < windowHeight / 2 && rect.bottom > windowHeight / 2) {
if (currentPage.value !== i) {
currentPage.value = i;
emit('pageChange', i, pdfPageCount.value);
}
}
}
})
.exec();
}
// ================== ZOOM MODE ==================
const toggleZoom = (pageNum: number) => {
if (!hasImage(pageNum)) return;
isZoomed.value[pageNum] = true;
};
// 处理加载队列
setTimeout(() => {
processLoadingQueue();
}, 50);
const resetZoom = (pageNum: number) => {
isZoomed.value[pageNum] = false;
translateX.value[pageNum] = 0;
translateY.value[pageNum] = 0;
};
/**
* 添加到加载队列
*/
const addToLoadingQueue = (pageNumber: number) => {
if (!loadingQueue.value.includes(pageNumber) &&
!loadedPageSet.value.has(pageNumber) &&
!isPageLoading(pageNumber)) {
console.log(`添加页面 ${pageNumber} 到加载队列`);
loadingQueue.value.push(pageNumber);
// 限制队列长度,避免一次性加载太多
if (loadingQueue.value.length > 10) {
loadingQueue.value = loadingQueue.value.slice(0, 10);
}
}
// ================== DRAGGING ==================
const onTouchStart = (e: any, pageNum: number) => {
const touches = e.touches || [];
if (touches.length !== 1) return;
const x = touches[0].clientX - (translateX.value[pageNum] || 0);
const y = touches[0].clientY - (translateY.value[pageNum] || 0);
(window as any).pdfTouchStart = { x, y, pageNum };
};
/**
* 处理加载队列
*/
const processLoadingQueue = async () => {
if (isProcessingQueue.value || loadingQueue.value.length === 0) return;
const onTouchMove = (e: any, pageNum: number) => {
const touches = e.touches || [];
if (touches.length !== 1 || !isZoomed.value[pageNum]) return;
isProcessingQueue.value = true;
const start = (window as any).pdfTouchStart;
if (!start || start.pageNum !== pageNum) return;
try {
// 每次处理1页
const pagesToLoad = loadingQueue.value.splice(0, 1);
console.log(`处理加载队列: 加载页面 ${pagesToLoad[0]}`);
const currentX = touches[0].clientX - start.x;
const currentY = touches[0].clientY - start.y;
for (const pageNumber of pagesToLoad) {
await loadPdfPage(pageNumber);
}
const sys = uni.getSystemInfoSync();
const viewW = sys.windowWidth;
const viewH = sys.windowHeight;
const imgW = pageRenderWidth.value[pageNum] || 0;
const imgH = pageRenderHeight.value[pageNum] || 0;
} catch (err) {
console.error('处理加载队列失败:', err);
} finally {
isProcessingQueue.value = false;
// 如果队列中还有任务,继续处理
if (loadingQueue.value.length > 0) {
setTimeout(processLoadingQueue, 200);
} else {
// 队列处理完成后,再次检查可见页面
setTimeout(() => {
checkVisiblePages();
}, 300);
}
}
};
if (imgW === 0 || imgH === 0) return;
/**
* 加载PDF页面
*/
const loadPdfPage = async (pageNumber: number) => {
if (loadedPageSet.value.has(pageNumber)) return;
const retryCount = getPageRetryCount(pageNumber);
if (retryCount >= props.maxRetryCount) {
console.warn(`页面 ${pageNumber} 已达到最大重试次数`);
return;
}
try {
imgLoading.value[pageNumber - 1] = true;
currentLoading.value++;
const minX = viewW - imgW > 0 ? 0 : viewW - imgW;
const minY = viewH - imgH > 0 ? 0 : viewH - imgH;
const maxX = 0;
const maxY = 0;
console.log(`开始加载页面 ${pageNumber}...`);
translateX.value[pageNum] = Math.min(maxX, Math.max(minX, currentX));
translateY.value[pageNum] = Math.min(maxY, Math.max(minY, currentY));
};
if (!pdfDoc.value) {
throw new Error('PDF文档未加载');
}
const onTouchEnd = () => {
delete (window as any).pdfTouchStart;
};
const page = await pdfDoc.value.getPage(pageNumber);
// 根据设备像素比动态设置缩放
const pixelRatio = window.devicePixelRatio || 1;
const scale = Math.max(1.5, pixelRatio); // 至少 1.5 倍,高分屏自动更高
// ================== LAZY LOAD ==================
const onScroll = (e: any) => {
if (!props.lazyLoad || globalLoading.value) return;
scrollTop.value = e.detail.scrollTop;
const now = Date.now();
if (now - lastScroll.value < 200) return;
lastScroll.value = now;
checkVisible();
};
const viewport = page.getViewport({ scale });
// const viewport = page.getViewport({ scale: 1.8 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) {
throw new Error('无法获取Canvas上下文');
}
canvas.width = viewport.width;
canvas.height = viewport.height;
const renderContext = {
canvasContext: context,
viewport: viewport
};
await page.render(renderContext).promise;
const imageData = canvas.toDataURL('image/jpeg', 0.85);
pdfImages.value[pageNumber - 1] = imageData;
loadedPageSet.value.add(pageNumber);
console.log(`页面 ${pageNumber} 加载完成,当前已加载: ${Array.from(loadedPageSet.value).join(',')}`);
const checkVisible = () => {
if (totalPages.value === 0) return;
// 清理
canvas.width = 0;
canvas.height = 0;
const sys = uni.getSystemInfoSync();
const winHeight = sys.windowHeight;
const top = scrollTop.value;
// 清除失败记录
if (failedPages.value[pageNumber]) {
delete failedPages.value[pageNumber];
}
// 👇 关键:不依赖页面高度,直接按页码区间预加载
// 假设每页至少占 200px(保守值)
const MIN_PAGE_HEIGHT = 200; // px
} catch (err: any) {
console.error(`页面 ${pageNumber} 加载失败:`, err);
const visibleStartPage = Math.max(1, Math.floor(top / MIN_PAGE_HEIGHT));
const visibleEndPage = Math.min(
totalPages.value,
Math.ceil((top + winHeight * 2) / MIN_PAGE_HEIGHT)
);
// 记录失败次数
if (!failedPages.value[pageNumber]) {
failedPages.value[pageNumber] = 1;
} else {
failedPages.value[pageNumber]++;
}
// 预加载前后各 2 页(共约 5~7 页)
const startPage = Math.max(1, visibleStartPage - 2);
const endPage = Math.min(totalPages.value, visibleEndPage + 2);
// 对有问题的页面生成占位图
if (err.message.includes('private field') || err.message.includes('TypeError')) {
pdfImages.value[pageNumber - 1] = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjUwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjhmOWZhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuW3suino+eggTwvdGV4dD48L3N2Zz4=';
loadedPageSet.value.add(pageNumber);
for (let i = startPage; i <= endPage; i++) {
if (!loadedSet.value.has(i) && !isLoading(i) && !isFailed(i)) {
addToQueue(i);
}
} finally {
imgLoading.value[pageNumber - 1] = false;
currentLoading.value--;
}
};
/**
* 手动加载下一页
*/
const loadNextPage = () => {
if (pdfPageCount.value === 0) return;
// 找到第一个未加载的页面
for (let i = 1; i <= pdfPageCount.value; i++) {
if (!loadedPageSet.value.has(i) && !isPageLoading(i)) {
console.log(`手动加载页面 ${i}`);
addToLoadingQueue(i);
processLoadingQueue();
break;
}
if (queue.value.length > 0 && !isProcessingQueue.value) {
processQueue();
}
};
/**
* 重试加载页面
*/
const retryLoadPage = (pageNumber: number) => {
const retryCount = getPageRetryCount(pageNumber);
if (retryCount >= props.maxRetryCount) {
uni.showToast({
title: '已达到最大重试次数',
icon: 'none',
duration: 2000
});
return;
}
if (failedPages.value[pageNumber]) {
delete failedPages.value[pageNumber];
// ================== UTILS ==================
const retryPage = (pageNum: number) => {
if (getRetryCount(pageNum) < props.maxRetryCount) {
addToQueue(pageNum);
processQueue();
}
// 从已加载集合中移除
loadedPageSet.value.delete(pageNumber);
pdfImages.value[pageNumber - 1] = '';
addToLoadingQueue(pageNumber);
processLoadingQueue();
};
// ========================== 辅助方法 ==========================
const getPageImage = (pageIndex: number): string => {
return pdfImages.value[pageIndex - 1] || '';
};
const isPageLoading = (pageIndex: number): boolean => {
return imgLoading.value[pageIndex - 1] || false;
};
const isPageFailed = (pageIndex: number): boolean => {
const retryCount = getPageRetryCount(pageIndex);
return retryCount > 0 && retryCount <= props.maxRetryCount;
const getRetryCount = (pageNum: number) => failed.value[pageNum] || 0;
const reload = () => init();
const reset = () => {
images.value = [];
loading.value = [];
failed.value = {};
loadedSet.value.clear();
queue.value = [];
isZoomed.value = {};
translateX.value = {};
translateY.value = {};
pageOriginalWidth.value = {};
pageOriginalHeight.value = {};
pageRenderWidth.value = {};
pageRenderHeight.value = {};
globalError.value = false;
errorMessage.value = '';
};
const getPageRetryCount = (pageIndex: number): number => {
return failedPages.value[pageIndex] || 0;
const cleanup = () => {
if (pdfDoc.value) {
pdfDoc.value.destroy();
pdfDoc.value = null;
}
};
const handlePageImageLoad = (pageIndex: number) => {
console.log(`页面 ${pageIndex} 图片加载完成`);
const setError = (msg: string) => {
globalError.value = true;
errorMessage.value = msg;
};
const handlePageImageError = (pageIndex: number) => {
console.error(`页面 ${pageIndex} 图片加载失败`);
const getPageMinHeight = (pageNum: number) => {
if (loadedSet.value.has(pageNum)) {
const w = pageRenderWidth.value[pageNum] || 1;
const h = pageRenderHeight.value[pageNum] || 1;
return uni.getSystemInfoSync().windowWidth * (h / w);
}
return 400; // px
};
// 暴露方法给父组件
// ================== EXPOSE ==================
defineExpose({
initPdf,
reload,
loadNextPage, // 新增手动加载下一页方法
getCurrentPage: () => currentPage.value,
getTotalPages: () => pdfPageCount.value,
getLoadedPages: () => Array.from(loadedPageSet.value),
// 手动控制横屏提示
showLandscapeTip: () => autoLandscapeTipRef.value?.show?.(),
hideLandscapeTip: () => autoLandscapeTipRef.value?.hide?.(),
resetLandscapeTip: () => autoLandscapeTipRef.value?.reset?.()
init,
});
</script>
<style scoped lang="scss">
.pdf-viewer {
width: 100%;
height: 100vh;
background: #ffffff;
display: flex;
flex-direction: column;
}
.pdf-scroll-view {
flex: 1;
height: 0; // 重要:让 scroll-view 正确计算高度
background: #fff;
}
.pdf-info {
display: flex;
justify-content: space-between;
align-items: center;
.pdf-header {
padding: 24rpx;
background: #f8f9fa;
border-bottom: 1rpx solid #e8e8e8;
.pdf-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
flex: 1;
}
.pdf-page-count {
font-size: 26rpx;
color: #666;
background: #e6f7ff;
padding: 8rpx 16rpx;
border-radius: 20rpx;
}
font-weight: bold;
}
.page-container {
margin-bottom: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.pdf-scroll {
width: 100%;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 24rpx;
background: #fafafa;
.page-number {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.page-item {
position: relative;
margin-bottom: 10rpx;
padding: 0 24rpx;
}
.page-status {
.page-number-tag {
position: absolute;
top: -40rpx;
left: 24rpx;
background: #20269B;
color: white;
padding: 4rpx 12rpx;
border-radius: 20rpx;
font-size: 24rpx;
&.success {
color: #52c41a;
}
&.error {
color: #ff4d4f;
}
}
z-index: 10;
}
.page-content {
position: relative;
min-height: 400rpx;
.pdf-image {
.pdf-image-fit {
width: 100%;
height: auto;
display: block;
// 禁用长按菜单
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
// 禁止长按保存
pointer-events: none;
}
border-radius: 12rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
}
.loadEffect {
width: 200rpx;
height: 200rpx;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: url('../../static/range-fullloading/loading.gif') no-repeat center;
background-size: contain;
z-index: 10;
}
.page-error {
.placeholder {
width: 100%;
height: 400rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400rpx;
background: #fff2f2;
border: 1rpx dashed #ff4d4f;
border-radius: 8rpx;
color: #ff4d4f;
.error-text {
align-items: center;
background: #f9f9f9;
border-radius: 12rpx;
font-size: 28rpx;
margin-bottom: 16rpx;
}
.retry-count {
font-size: 24rpx;
color: #999;
}
}
.page-placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 400rpx;
background: #f8f9fa;
color: #666;
font-size: 28rpx;
.placeholder.error {
color: #ff4d4f;
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 0;
.zoom-container {
overflow: hidden;
background: #f9f9f9;
border-radius: 12rpx;
min-height: 400rpx;
}
.loading-spinner {
width: 40rpx;
height: 40rpx;
border: 4rpx solid #e8e8e8;
border-top: 4rpx solid #20269B;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 24rpx;
}
.image-original {
position: relative;
transition: transform 0.1s ease-out;
}
text {
color: #666;
font-size: 28rpx;
}
.reset-btn {
position: absolute;
bottom: 20rpx;
right: 20rpx;
background: rgba(0,0,0,0.7);
color: white;
padding: 12rpx 20rpx;
border-radius: 6rpx;
font-size: 24rpx;
z-index: 999;
}
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 0;
.global-status {
text-align: center;
.error-icon {
font-size: 60rpx;
margin-bottom: 24rpx;
}
.error-message {
color: #ff4d4f;
font-size: 28rpx;
margin-bottom: 32rpx;
padding: 80rpx 0;
color: #666;
.spinner {
margin: 0 auto 16rpx;
}
.retry-button {
}
.global-status.error button {
margin-top: 20rpx;
background: #20269B;
color: white;
border: none;
padding: 16rpx 32rpx;
border-radius: 8rpx;
font-size: 28rpx;
}
}
.progress-info {
padding: 12rpx;
background: #f8f9fa;
.progress-bar {
padding: 20rpx;
text-align: center;
text {
display: block;
color: #666;
font-size: 26rpx;
margin-bottom: 16rpx;
}
}
.progress-bar {
color: #666;
.bar-bg {
width: 100%;
height: 8rpx;
background: #e8e8e8;
background: #eee;
border-radius: 4rpx;
overflow: hidden;
.progress-inner {
margin-top: 8rpx;
.bar-fill {
height: 100%;
background: #20269B;
transition: width 0.3s ease;
transition: width 0.3s;
}
}
}
.action-buttons {
.spinner {
width: 40rpx;
height: 40rpx;
border: 4rpx solid #eee;
border-top: 4rpx solid #20269B;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.action-btns {
display: flex;
justify-content: space-between;
padding: 20rpx 16rpx;
background: #f8f9fa;
border-top: 1rpx solid #e8e8e8;
justify-content: center;
margin-top: 20rpx;
}
.action-btn {
flex: 1;
margin: 0 8rpx;
.zoom-btn, .reset-btn-inline {
padding: 8rpx 24rpx;
font-size: 24rpx;
border-radius: 8rpx;
background: #20269B;
color: white;
border: none;
padding: 16rpx;
border-radius: 8rpx;
font-size: 26rpx;
line-height: 1;
}
&:disabled {
background: #ccc;
color: #999;
}
}
.reset-btn-inline {
background: #666;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
to { transform: rotate(360deg); }
}
</style>
\ No newline at end of file
......@@ -107,6 +107,7 @@
uni.setStorageSync('isLogin', '1');
uni.setStorageSync('loginType', 'codelogin');
uni.setStorageSync('cffp_userId', res.data['userId']);
console.log('============',uni.getStorageSync('cffp_userId'))
uni.setStorageSync('uni-token', res.data['token']);
//关闭弹窗
this.$refs.loginPopup.close();
......
......@@ -53,7 +53,7 @@ const config = {
stage,
prod
}
let env = 'prod';
let env = 'dev';
let baseURL = config[env].base_url;
let apiURL = config[env].api_url;
......
......@@ -98,6 +98,8 @@
</view>
<!-- 放在 </view> 最后,</template> 之前 -->
<!-- PDF 查看弹窗 -->
<!-- 调试用 -->
<view v-if="showPdfModal">Debug URL: {{ currentPdfUrl }}</view>
<uni-popup
ref="pdfPopupRef"
:mask-click="true"
......@@ -111,14 +113,15 @@
</view>
<!-- PDF 查看器 -->
<view class="pdf-viewer-wrapper" v-if="showPdfModal">
<view class="pdf-viewer-wrapper" v-if="showPdfModal && currentPdfUrl">
<PdfViewer
:pdfInfo="{ url: currentPdfUrl }"
:autoLoad="true"
:lazyLoad="true"
:lazyLoad="false"
:maxRetryCount="2"
:loadingStatus="true"
style="height: 100%; width: 100%;"
@loadComplete="handlePdfLoadComplete"
@loadError="handlePdfLoadError"
@pageChange="handlePageChange"
/>
</view>
</view>
......@@ -130,11 +133,10 @@ import { ref, computed, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import common from '@/common/common';
import api from '@/api/api';
import { onBeforeUnmount } from 'vue';
// 导入PDF查看器组件
import PdfViewer from '@/components/pdf-viewer/pdf-viewer.vue';
import { onBeforeUnmount } from 'vue';
import uniPopup from '@dcloudio/uni-ui/lib/uni-popup/uni-popup.vue';
const pdfPopupRef = ref();
// 路由实例
const router = useRouter();
......@@ -294,16 +296,21 @@ const navigateToPKPage = () => {
const showPdfModal = ref(false);
const currentPdfUrl = ref('');
// 修改 getUrl 方法
const getUrl = (fileUrl) => {
if (!fileUrl) {
uni.showToast({ title: '暂无文档', icon: 'none' });
return;
}
uni.showLoading({ title: '加载PDF中...' });
// 所有平台统一处理:弹出 PdfViewer
uni.showLoading({ title: '加载PDF中...' });
currentPdfUrl.value = fileUrl;
showPdfModal.value = true;
console.log('Opening PDF:', fileUrl);
// 延迟打开弹窗,确保数据已绑定
setTimeout(() => {
pdfPopupRef.value?.open?.();
uni.hideLoading();
}, 100);
};
// 关闭弹窗
......@@ -587,6 +594,9 @@ onMounted(() => {
border-radius: 50%;
border: none;
color: #999;
display: flex;
justify-content: center;
align-items: center;
}
.pdf-viewer-wrapper {
......
......@@ -764,10 +764,9 @@
}
api.loginVerification(params).then((res)=>{
if(res['success']){
uni.setStorageSync('isLogin','1');
uni.setStorageSync('loginType','codelogin');
uni.setStorageSync('cffp_userId', res.data.userId);
uni.setStorageSync('cffp_userId', JSON.stringify(res.data.userId));
uni.setStorageSync('uni-token', res.data['token']);
this.userId = res.data.userId
this.querySystemMessage()
......
......@@ -130,7 +130,8 @@ const companyPdf = ref<PdfItem>({
// urls: Array.from({ length: 21 }, (_, i) =>
// `${OSS_BASE_URL}/public/company-intro_part${i + 1}.pdf`
// ),
urls: [`${OSS_BASE_URL}/public/company-intro.pdf`],
// urls: [`${OSS_BASE_URL}/public/company-intro.pdf`],
urls: [`${OSS_BASE_URL}/wslucky/product/2025/06/24/31c164ac-565c-4990-a584-b5d4935840d0.pdf`],
type: 'showURL'
});
......@@ -310,7 +311,7 @@ const switchTab = (index: number) => {
const tabs = filteredCurrentTabs.value;
if (index < 0 || index >= tabs.length || tabs.length === 0) return;
uni.showLoading({ title: '切换中...' });
loading.value = true;
setTimeout(() => {
activeTab.value = index;
......@@ -318,7 +319,7 @@ const switchTab = (index: number) => {
uni.setStorageSync('tabsIndex', index);
setTimeout(() => {
uni.hideLoading();
loading.value = false;
}, 300);
}, 100);
};
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment