Commit c9fc0671 by Sweet Zhang

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

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