Commit a179a568 by yuzhenWang

Merge branch 'dev' into 'feature-20260203-解决外部跳转页面登录问题'

Dev

See merge request !81
parents a5333080 de2a5c14
...@@ -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,504 @@ ...@@ -82,778 +114,504 @@
</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 onOrientationChange = (orientation: 'portrait' | 'landscape') => {
console.log('屏幕方向变为:', orientation);
};
// 全局状态
const globalLoading = ref(false);
const globalError = ref(false);
const errorMessage = ref('');
// ========================== 计算属性 ========================== // ================== COMPUTED ==================
const loadedPages = computed(() => { const progress = computed(() =>
return loadedPageSet.value.size; totalPages.value > 0 ? (loadedSet.value.size / totalPages.value) * 100 : 0
}); );
const hasMoreToLoad = computed(() => { const hasImage = (pageNum: number) => !!images.value[pageNum - 1];
return loadedPages.value < pdfPageCount.value; 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;
// ========================== 生命周期 ========================== // ================== LIFECYCLE ==================
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();
});
// ========================== 监听器 ==========================
watch(() => props.pdfInfo.url, (newUrl, oldUrl) => {
if (newUrl && newUrl !== oldUrl) {
resetState();
initPdf();
}
}); });
// ========================== 公共方法 ========================== // ================== INIT ==================
/** const init = async () => {
* 初始化PDF if (!props.pdfInfo.url) return setError('PDF URL 为空');
*/
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();
};
// #ifdef H5
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker; // ← 静态导入
useWorkerFlag = true;
// #endif
const doc = await pdfjsLib.getDocument({
// ========================== 内部方法 ==========================
/**
* 重置状态
*/
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) {
pdfDoc.value.destroy();
pdfDoc.value = null;
}
};
/**
* 清理资源
*/
const cleanup = () => {
if (pdfDoc.value) {
pdfDoc.value.destroy();
pdfDoc.value = null;
}
};
/**
* 设置错误状态
*/
const setError = (message: string) => {
error.value = true;
errorMessage.value = message;
loading.value = false;
};
/**
* 加载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 {
const visibleTop = scrollTop - 500; // 提前500px开始加载 loading.value[pageNum - 1] = true;
const visibleBottom = scrollTop + windowHeight + 1000; // 延后1000px加载 const page = await pdfDoc.value.getPage(pageNum);
// 检查每个页面是否在可见区域内 // 获取原始尺寸
for (let i = 1; i <= pdfPageCount.value; i++) { const originalViewport = page.getViewport({ scale: 1 });
// 如果页面已经加载或正在加载,跳过 pageOriginalWidth.value[pageNum] = originalViewport.width;
if (loadedPageSet.value.has(i) || isPageLoading(i)) { pageOriginalHeight.value[pageNum] = originalViewport.height;
continue;
// 计算高清渲染尺寸(目标宽度 ~1600px)
const targetPhysicalWidth = 1600;
const scale = Math.min(5.0, Math.max(1.0, targetPhysicalWidth / originalViewport.width));
const renderViewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
console.error('Failed to get 2D context for canvas.');
return;
} }
canvas.width = renderViewport.width;
canvas.height = renderViewport.height;
await page.render({ canvasContext: ctx, viewport: renderViewport }).promise;
// 检查页面位置 // 存储渲染结果
uni.createSelectorQuery() pageRenderWidth.value[pageNum] = canvas.width;
.select(`#page-${i}`) pageRenderHeight.value[pageNum] = canvas.height;
.boundingClientRect((rect: any) => { images.value[pageNum - 1] = canvas.toDataURL('image/jpeg', 0.95);
if (rect) { loadedSet.value.add(pageNum);
const pageTop = scrollTop + rect.top; delete failed.value[pageNum];
const pageBottom = scrollTop + rect.bottom;
// 如果页面在可见区域内
if (pageBottom > visibleTop && pageTop < visibleBottom) {
console.log(`页面 ${i} 在可见区域内,准备加载`);
addToLoadingQueue(i);
}
// 更新当前页 canvas.width = canvas.height = 0;
if (rect.top < windowHeight / 2 && rect.bottom > windowHeight / 2) { } catch (err) {
if (currentPage.value !== i) { console.error(`Page ${pageNum} error:`, err);
currentPage.value = i; failed.value[pageNum] = (failed.value[pageNum] || 0) + 1;
emit('pageChange', i, pdfPageCount.value); } finally {
} if (isMounted.value) loading.value[pageNum - 1] = false;
}
}
})
.exec();
}
// 处理加载队列
setTimeout(() => {
processLoadingQueue();
}, 50);
};
/**
* 添加到加载队列
*/
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);
}
} }
}; };
/** // ================== ZOOM MODE ==================
* 处理加载队列 const toggleZoom = (pageNum: number) => {
*/ if (!hasImage(pageNum)) return;
const processLoadingQueue = async () => { isZoomed.value[pageNum] = true;
if (isProcessingQueue.value || loadingQueue.value.length === 0) return; };
isProcessingQueue.value = true; const resetZoom = (pageNum: number) => {
isZoomed.value[pageNum] = false;
translateX.value[pageNum] = 0;
translateY.value[pageNum] = 0;
};
try { // ================== DRAGGING ==================
// 每次处理1页 const onTouchStart = (e: any, pageNum: number) => {
const pagesToLoad = loadingQueue.value.splice(0, 1); const touches = e.touches || [];
console.log(`处理加载队列: 加载页面 ${pagesToLoad[0]}`); 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 };
};
for (const pageNumber of pagesToLoad) { const onTouchMove = (e: any, pageNum: number) => {
await loadPdfPage(pageNumber); const touches = e.touches || [];
} if (touches.length !== 1 || !isZoomed.value[pageNum]) return;
} catch (err) { const start = (window as any).pdfTouchStart;
console.error('处理加载队列失败:', err); if (!start || start.pageNum !== pageNum) return;
} finally {
isProcessingQueue.value = false;
// 如果队列中还有任务,继续处理 const currentX = touches[0].clientX - start.x;
if (loadingQueue.value.length > 0) { const currentY = touches[0].clientY - start.y;
setTimeout(processLoadingQueue, 200);
} else {
// 队列处理完成后,再次检查可见页面
setTimeout(() => {
checkVisiblePages();
}, 300);
}
}
};
/** const sys = uni.getSystemInfoSync();
* 加载PDF页面 const viewW = sys.windowWidth;
*/ const viewH = sys.windowHeight;
const loadPdfPage = async (pageNumber: number) => { const imgW = pageRenderWidth.value[pageNum] || 0;
if (loadedPageSet.value.has(pageNumber)) return; const imgH = pageRenderHeight.value[pageNum] || 0;
const retryCount = getPageRetryCount(pageNumber); if (imgW === 0 || imgH === 0) return;
if (retryCount >= props.maxRetryCount) {
console.warn(`页面 ${pageNumber} 已达到最大重试次数`);
return;
}
try { const minX = viewW - imgW > 0 ? 0 : viewW - imgW;
imgLoading.value[pageNumber - 1] = true; const minY = viewH - imgH > 0 ? 0 : viewH - imgH;
currentLoading.value++; 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) { 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 { .pdf-header {
flex: 1;
height: 0; // 重要:让 scroll-view 正确计算高度
}
.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 {
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; 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();
......
...@@ -26,11 +26,10 @@ ...@@ -26,11 +26,10 @@
<!-- 对比内容(按group分组展示) --> <!-- 对比内容(按group分组展示) -->
<!-- 对比内容:从 productPKInfoList.basicInfos 提取分组渲染 --> <!-- 对比内容:从 productPKInfoList.basicInfos 提取分组渲染 -->
<view class="compare-content" v-if="productPKInfoList.length > 0"> <view class="compare-content" v-if="productPKInfoList.length > 0 && renderGroups.length > 0">
<!-- 核心:从第一个产品的 basicInfos 中遍历分组(A/B/C组) -->
<view <view
class="compare-section" class="compare-section"
v-for="group in getFirstProductGroups()" v-for="group in renderGroups"
:key="group.groupCode" :key="group.groupCode"
> >
<view class="section-header"> <view class="section-header">
...@@ -42,24 +41,23 @@ ...@@ -42,24 +41,23 @@
<text class="section-title">{{ group.groupName }}</text> <text class="section-title">{{ group.groupName }}</text>
</view> </view>
<view class="section-content"> <view class="section-content">
<!-- 遍历当前分组下的所有属性(factor) -->
<view <view
class="compare-item" class="compare-item"
:class="{ 'hidden-item': shouldHide(group.groupCode, factor.type) }" :class="{ 'hidden-item': shouldHide(field.fieldBizId) }"
v-for="factor in group.factors" v-for="field in group.fields"
:key="factor.type" :key="field.fieldBizId"
> >
<text class="item-label">{{ factor.typeName }}</text> <text class="item-label">{{ field.label }}</text>
<view class="item-values"> <view class="item-values">
<!-- 遍历所有产品,匹配当前分组+当前属性的内容 -->
<view class="item-value" v-for="product in productPKInfoList" :key="product.planBizId"> <view class="item-value" v-for="product in productPKInfoList" :key="product.planBizId">
{{ getProductFactorValue(product, group.groupCode, factor.type) || '-' }} {{ getProductFieldValue(product, field.fieldBizId) || '-' }}
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
<!-- 产品彩页分组(单独处理,从 planFiles 提取) --> <!-- 产品彩页分组(单独处理,从 planFiles 提取) -->
<view class="compare-section" id="product-file-group"> <view class="compare-section" id="product-file-group">
<view class="section-header"> <view class="section-header">
...@@ -96,6 +94,36 @@ ...@@ -96,6 +94,36 @@
<text class="empty-text">暂无对比产品数据</text> <text class="empty-text">暂无对比产品数据</text>
</view> </view>
</view> </view>
<!-- 放在 </view> 最后,</template> 之前 -->
<!-- PDF 查看弹窗 -->
<!-- 调试用 -->
<view v-if="showPdfModal">Debug URL: {{ currentPdfUrl }}</view>
<uni-popup
ref="pdfPopupRef"
:mask-click="true"
type="bottom"
@change="onPopupChange"
>
<view class="pdf-modal-container">
<!-- 关闭按钮 -->
<view class="modal-header">
<button class="close-btn" @click="closePdfModal"></button>
</view>
<!-- PDF 查看器 -->
<view class="pdf-viewer-wrapper" v-if="showPdfModal && currentPdfUrl">
<PdfViewer
:pdfInfo="{ url: currentPdfUrl }"
:autoLoad="true"
:lazyLoad="false"
:maxRetryCount="2"
@loadComplete="handlePdfLoadComplete"
@loadError="handlePdfLoadError"
@pageChange="handlePageChange"
/>
</view>
</view>
</uni-popup>
</template> </template>
<script setup> <script setup>
...@@ -103,165 +131,137 @@ import { ref, computed, onMounted } from 'vue'; ...@@ -103,165 +131,137 @@ 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';
// 路由实例 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 { hshare } from '@/util/fiveshare';
const pdfPopupRef = ref();
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
// 后端返回的原始数据 // 原始数据
const resultData = ref({}); const resultData = ref({});
// 配置分组列表(基本信息、适合人群等) const configList = ref([]); // ← 新增:用于定义页面结构
const configList = ref([]);
// 产品对比信息列表
const productPKInfoList = ref([]); const productPKInfoList = ref([]);
// 是否只看不同
const showOnlyDiff = ref(false); const showOnlyDiff = ref(false);
// 查看文件的URL const currentPdfUrl = ref('');
const viewFileUrl = ref() const showPdfModal = ref(false);
// 【✅ 关键修正1:构建 fieldBizId → 字段元信息 的映射】
const fieldMetaMap = ref(new Map()); // fieldBizId => { cnName, groupCode }
// 【✅ 关键修正2:初始化 configList 并构建映射】
const initConfigStructure = (configs) => {
configList.value = configs;
const map = new Map();
configs.forEach(group => {
group.fieldList.forEach(field => {
map.set(field.fieldBizId, {
cnName: field.fieldCnName,
groupCode: group.groupCode,
groupName: group.groupName
});
});
});
fieldMetaMap.value = map;
};
// 【✅ 关键修正3:获取标准化的分组+字段结构(用于渲染)】
const renderGroups = computed(() => {
return configList.value.map(group => ({
groupCode: group.groupCode,
groupName: group.groupName,
fields: group.fieldList.map(field => ({
fieldBizId: field.fieldBizId,
label: field.fieldCnName
}))
}));
});
// 【✅ 关键修正4:根据 fieldBizId 获取产品值】
const getProductFieldValue = (product, fieldBizId) => {
for (const basicInfo of product.basicInfos || []) {
const factor = basicInfo.factors?.find(f => f.type === fieldBizId);
if (factor) return factor.content || '';
}
return ''; // 未找到返回空
};
// 【✅ 关键修正5:判断是否应隐藏(只看不同)】
const shouldHide = (fieldBizId) => {
if (!showOnlyDiff.value || productPKInfoList.value.length <= 1) return false;
const normalize = (val) => (val === '' || val === '/' ? '无数据' : val);
const baseValue = normalize(getProductFieldValue(productPKInfoList.value[0], fieldBizId));
return productPKInfoList.value.every(product => {
return normalize(getProductFieldValue(product, fieldBizId)) === baseValue;
});
};
// 根据分组编码获取对应的图标 // 其余方法保持不变(图标、颜色、PDF、分享等)
const getGroupIcon = (groupCode) => { const getGroupIcon = (groupCode) => {
const iconMap = { const iconMap = { A: 'info', B: 'auth', C: 'star', D: 'image' };
'A': 'info', // 基本信息
'B': 'auth', // 适合人群
'C': 'star', // 产品特色
'D': 'image' // 产品彩页
};
return iconMap[groupCode] || 'help'; return iconMap[groupCode] || 'help';
}; };
// 根据分组编码获取对应的颜色
const getGroupColor = (groupCode) => { const getGroupColor = (groupCode) => {
const colorMap = { const colorMap = { A: '#20269B', B: '#00cc66', C: '#ff9900', D: '#cc66ff' };
'A': '#20269B', // 基本信息-蓝色
'B': '#00cc66', // 适合人群-绿色
'C': '#ff9900', // 产品特色-橙色
'D': '#cc66ff' // 产品彩页-紫色
};
return colorMap[groupCode] || '#999'; return colorMap[groupCode] || '#999';
}; };
import {hshare} from '@/util/fiveshare';
const wxShare = (productIds,categoryId)=>{
// H5 自定义分享
const shareLink = `${window.location.origin}/myPackageA/compare-result/compare-result?categoryId=${categoryId}&productIds=${productIds}`;
// 2. 分享标题(简洁明了,包含产品数量) const wxShare = (productIds, categoryId) => {
const shareLink = `${window.location.origin}/myPackageA/compare-result/compare-result?categoryId=${categoryId}&productIds=${productIds}`;
const shareTitle = `多款产品对比结果`; const shareTitle = `多款产品对比结果`;
// 3. 分享描述(突出对比价值)
const shareDesc = `包含核心参数对比,快速了解差异`; const shareDesc = `包含核心参数对比,快速了解差异`;
// 4. 分享图标(建议用公司LOGO或产品相关图标,尺寸200x200px,HTTPS地址)
const shareIcon = `${window.location.origin}/static/mypoint_pic.png`; const shareIcon = `${window.location.origin}/static/mypoint_pic.png`;
let data = { let data = { title: shareTitle, desc: shareDesc, link: shareLink, imgUrl: shareIcon };
title: shareTitle,
desc:shareDesc,
link: shareLink, //分享链接
imgUrl: shareIcon, //图片c
}
//安卓机型获取当前页面路径
let url = window.location.href.split('#')[0]; let url = window.location.href.split('#')[0];
//ios机型获取当前页面路径
let ua = navigator.userAgent.toLowerCase(); let ua = navigator.userAgent.toLowerCase();
let isWeixin = ua.indexOf('micromessenger') !== -1; let isWeixin = ua.indexOf('micromessenger') !== -1;
if (isWeixin) { if (isWeixin) {
let isiOS = /(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent); //ios终端 let isiOS = /(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent);
if (isiOS && window.sessionStorage.getItem('firstEntryUrl')) { if (isiOS && window.sessionStorage.getItem('firstEntryUrl')) {
url = window.sessionStorage.getItem('firstEntryUrl').split('#')[0]; url = window.sessionStorage.getItem('firstEntryUrl').split('#')[0];
} }
} }
hshare(data, url) hshare(data, url);
} };
const navigateToPKPage = () => {
uni.navigateBack();
};
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中...' });
// 区分环境:H5 端直接打开 URL,其他端用原下载逻辑 currentPdfUrl.value = fileUrl;
if (uni.getSystemInfoSync().uniPlatform === 'web') { showPdfModal.value = true;
// H5 方案:用浏览器新窗口打开 PDF(依赖浏览器原生支持) setTimeout(() => {
const opened = window.open(fileUrl, '_blank'); pdfPopupRef.value?.open?.();
} else { uni.hideLoading();
// 非 H5 端(小程序/APP):保留原下载+打开逻辑 }, 100);
uni.downloadFile({
url: fileUrl,
success: (res) => {
if (res.statusCode === 200) {
uni.openDocument({
filePath: res.tempFilePath,
showMenu: true,
success: () => console.log('打开文档成功'),
fail: (err) => {
uni.showToast({ title: '打开失败,请重试', icon: 'none' });
console.error('openDocument 失败:', err);
}
});
} else {
uni.showToast({ title: '下载失败', icon: 'none' });
}
},
fail: (err) => {
uni.showToast({ title: '下载失败,请检查网络', icon: 'none' });
console.error('downloadFile 失败:', err);
}
});
}
};
// 【核心1:获取第一个产品的分组列表(A/B/C组)】
// 所有产品的分组结构一致,取第一个产品的 basicInfos 作为分组源
const getFirstProductGroups = () => {
if (productPKInfoList.value.length === 0) return [];
return productPKInfoList.value[0].basicInfos || [];
}; };
// 【核心2:获取指定产品、指定分组、指定属性的内容】 const closePdfModal = () => {
const getProductFactorValue = (product, groupCode, factorType) => { showPdfModal.value = false;
// 1. 找到产品中当前分组(如A组-基本信息) setTimeout(() => pdfPopupRef.value?.close?.(), 100);
const targetGroup = product.basicInfos.find(item => item.groupCode === groupCode);
if (!targetGroup) return '';
// 2. 找到分组中当前属性(如type=5-保障期限)
const targetFactor = targetGroup.factors.find(item => item.type === factorType);
return targetFactor ? targetFactor.content : '';
}; };
const onPopupChange = (e) => {
// 【核心3:判断属性是否所有产品都相同(“只看不同”逻辑)】 if (!e.show && showPdfModal.value) showPdfModal.value = false;
const shouldHide = (groupCode, factorType) => {
// 未开启“只看不同”或只有1个产品,不隐藏
if (!showOnlyDiff.value || productPKInfoList.value.length <= 1) return false;
// 取第一个产品的属性值作为基准
const firstProduct = productPKInfoList.value[0];
const baseValue = getProductFactorValue(firstProduct, groupCode, factorType);
// 归一化空值(避免“/”“空字符串”视为不同)
const normalize = (val) => val === '' || val === '/' ? '无数据' : val;
const baseNormalized = normalize(baseValue);
// 检查所有产品的当前属性值是否与基准一致
const allSame = productPKInfoList.value.every(product => {
const currentValue = getProductFactorValue(product, groupCode, factorType);
return normalize(currentValue) === baseNormalized;
});
// 所有产品相同则隐藏,否则显示
return allSame;
}; };
onBeforeUnmount(() => {
if (pdfPopupRef.value) pdfPopupRef.value.close();
});
// 前往PK选择页 // 【✅ 关键修正6:初始化时同时处理 configList 和 productPKInfoList】
const navigateToPKPage = () => {
uni.navigateBack()
};
// 初始化数据
onMounted(() => { onMounted(() => {
const { productIds, categoryId } = route.query;
const { productIds, categoryId} = route.query;
if (!productIds || !categoryId) { if (!productIds || !categoryId) {
uni.showToast({ title: '缺少对比参数', icon: 'none' }); uni.showToast({ title: '缺少对比参数', icon: 'none' });
return; return;
...@@ -271,21 +271,22 @@ onMounted(() => { ...@@ -271,21 +271,22 @@ onMounted(() => {
category: categoryId, category: categoryId,
planBizIdList: productIds.split(',') planBizIdList: productIds.split(',')
}; };
let ua = navigator.userAgent.toLowerCase(); let ua = navigator.userAgent.toLowerCase();
let isWeixin = ua.indexOf('micromessenger') !== -1; let isWeixin = ua.indexOf('micromessenger') !== -1;
api.getProductPKInfo(params).then(res => { api.getProductPKInfo(params).then(res => {
if (res.success && res.data?.productPKInfoList) { if (res.success && res.data) {
productPKInfoList.value = res.data.productPKInfoList; // ✅ 同时保存 configList 和 product 列表
initConfigStructure(res.data.configList || []);
productPKInfoList.value = res.data.productPKInfoList || [];
// 微信环境初始化分享(如有) if (isWeixin) wxShare(productIds, categoryId);
if (isWeixin) {
wxShare(productIds,categoryId)
}
} else { } else {
common.errorDialog(1, res.message || '获取对比数据失败'); common.errorDialog(1, res.message || '获取对比数据失败');
} }
}); });
}) });
</script> </script>
<style scoped> <style scoped>
...@@ -488,4 +489,38 @@ onMounted(() => { ...@@ -488,4 +489,38 @@ onMounted(() => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.pdf-modal-container {
width: 100vw;
height: 90vh; /* 占屏 90% */
background: #fff;
border-top-left-radius: 20rpx;
border-top-right-radius: 20rpx;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 20rpx;
text-align: right;
}
.close-btn {
width: 60rpx;
height: 60rpx;
font-size: 36rpx;
background: #f5f5f5;
border-radius: 50%;
border: none;
color: #999;
display: flex;
justify-content: center;
align-items: center;
}
.pdf-viewer-wrapper {
flex: 1;
width: 100%;
overflow: hidden;
}
</style> </style>
\ No newline at end of file
...@@ -160,7 +160,7 @@ const fetchRateData = async () => { ...@@ -160,7 +160,7 @@ const fetchRateData = async () => {
try { try {
const params = { const params = {
planBizId:planBizId.value, planBizId:planBizId.value,
userId:localStorage.getItem('cffp_userId') userId:uni.getStorageSync('cffp_userId') || ''
} }
const response = await api.queryRate(params) const response = await api.queryRate(params)
...@@ -193,7 +193,7 @@ const fetchRateData = async () => { ...@@ -193,7 +193,7 @@ const fetchRateData = async () => {
// 生命周期 // 生命周期
onMounted(() => { onMounted(() => {
userId.value = localStorage.getItem('cffp_userId') || '' userId.value = uni.getStorageSync('cffp_userId') || ''
fetchRateData() fetchRateData()
}) })
</script> </script>
......
...@@ -131,7 +131,7 @@ ...@@ -131,7 +131,7 @@
<button <button
class="compare-btn" class="compare-btn"
@click="navigateToPKPage" @click="navigateToPKPage"
:disabled="pkList.length < 2" :disabled="pkList.length < 1"
> >
去对比 去对比
</button> </button>
......
...@@ -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'
}); });
...@@ -271,7 +272,6 @@ const queryUserInfoAndPermission = () => { ...@@ -271,7 +272,6 @@ const queryUserInfoAndPermission = () => {
const initPdfAfterPermission = () => { const initPdfAfterPermission = () => {
loading.value = true; loading.value = true;
const filteredTabs = filteredCurrentTabs.value; const filteredTabs = filteredCurrentTabs.value;
console.log(filteredTabs)
if (currentType.value >= 2 && currentType.value <= 4) { if (currentType.value >= 2 && currentType.value <= 4) {
if (filteredTabs.length > 0) { if (filteredTabs.length > 0) {
activeTab.value = 0; activeTab.value = 0;
...@@ -310,7 +310,7 @@ const switchTab = (index: number) => { ...@@ -310,7 +310,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 +318,7 @@ const switchTab = (index: number) => { ...@@ -318,7 +318,7 @@ const switchTab = (index: number) => {
uni.setStorageSync('tabsIndex', index); uni.setStorageSync('tabsIndex', index);
setTimeout(() => { setTimeout(() => {
uni.hideLoading(); loading.value = false;
}, 300); }, 300);
}, 100); }, 100);
}; };
......
...@@ -11,9 +11,10 @@ ...@@ -11,9 +11,10 @@
<view class="headerTop"> <view class="headerTop">
<view class="" style="margin-right: 15rpx;"> <view class="" style="margin-right: 15rpx;">
<view class="myName" v-if="loginType == 'codelogin'"> <view class="myName" v-if="loginType == 'codelogin'">
{{showMyName || '点头像完善信息'}} {{showMyName || '点头像完善信息'}}
<text v-if="customerBasicInfo.partnerType" <text v-if="customerBasicInfo.partnerType"
class="typePartner">{{customerBasicInfo.partnerType}}</text> class="typePartner">{{customerBasicInfo.partnerType}}</text>
</view> </view>
<view class="myName" v-if="loginType == 'visitor'">游客</view> <view class="myName" v-if="loginType == 'visitor'">游客</view>
</view> </view>
...@@ -190,7 +191,7 @@ ...@@ -190,7 +191,7 @@
currentPage: 'personalCenter', currentPage: 'personalCenter',
customerBasicInfo: {}, customerBasicInfo: {},
loginornot: true, loginornot: true,
tabBarPadding: 0, tabBarPadding: 100,
settingItem: { settingItem: {
title: '系统设置', title: '系统设置',
icon: 'setting', icon: 'setting',
...@@ -233,6 +234,7 @@ ...@@ -233,6 +234,7 @@
}, },
], ],
}, },
{ {
id: '01', id: '01',
categoryName: '团队', categoryName: '团队',
...@@ -253,7 +255,6 @@ ...@@ -253,7 +255,6 @@
isShow: true, isShow: true,
identity: true identity: true
}, },
// {title:'我的团队',icon:'icon-tuandui',link:'/pages/personalCenter/myTeam',isOpen:true,isShow:true,identity: true},
{ {
title: '我的团队', title: '我的团队',
icon: 'icon-tuandui', icon: 'icon-tuandui',
...@@ -422,25 +423,22 @@ ...@@ -422,25 +423,22 @@
}); });
if (uni.getStorageSync('cffp_userInfo')) { if (uni.getStorageSync('cffp_userInfo')) {
this.userInfo = JSON.parse(uni.getStorageSync('cffp_userInfo')) this.userInfo = JSON.parse(uni.getStorageSync('cffp_userInfo'))
} if (this.userInfo) {
// 计算tabbar高度,避免tabbar遮挡页面底部内容 const levelCode = this.userInfo.levelCode || '';
const sysInfo = uni.getSystemInfoSync(); const dealerId = this.userInfo.dealerId || '';
this.tabBarPadding = 100; // 默认值,可根据你的设计调整 // 2. 找到“申请加盟”菜单项(通过 key)
if (sysInfo.windowBottom) { const team = this.mainMenuLists.find(item => item.id === '01');
// H5 平台下,windowBottom 表示可视区域底部到屏幕底的距离(即 tabbar 高度 + 安全区域) if (!team) return;
this.tabBarPadding = sysInfo.windowBottom;
} else { const applyItem = team.children.find(child => child.key === 'applyFranchise');
// 兜底逻辑:根据平台和设备估算 if (!applyItem) return;
if (sysInfo.platform === 'ios') {
// iPhone 底部安全区一般为 34px(全面屏)或 0(非全面屏) // 3. 更新标题和显示状态
const isIPhoneX = /iPhone X|iPhone 1[0-9]|iPhone [A-Z]/.test(sysInfo.model); applyItem.title = levelCode === '' ? '申请加盟' : '晋升目标';
this.tabBarPadding += isIPhoneX ? 34 : 0; applyItem.isShow = !dealerId && levelCode != 'P3'; // 没有 dealerId 才显示
}
// 如果是 iPad,横屏时底部安全区可能为 0,但 tabbar 仍存在
if (sysInfo.deviceType === 'tablet') {
this.tabBarPadding = 60; // iPad 常见 tabbar 高度
} }
} }
// #ifdef H5 // #ifdef H5
initJssdkShare(() => { initJssdkShare(() => {
setWechatShare(); setWechatShare();
......
...@@ -23,10 +23,8 @@ export default [ ...@@ -23,10 +23,8 @@ export default [
"Q": "合伙人晋升条件", "Q": "合伙人晋升条件",
"A": [ "A": [
"见习合伙人:完成加盟申请", "见习合伙人:完成加盟申请",
"新锐合伙人:个人标准销售额≥799元", "新锐合伙人:个人标准销售额≥198元",
"资深合伙人:个人标准销售额≥799元 + 团队有效人数≥5人 + 团队标准销售额≥5万元", "资深合伙人:个人标准销售额≥198元 + 团队有效人数≥5人 + 团队标准销售额≥5万元",
"精英合伙人:个人标准销售额≥799元 + 团队有效人数≥10人 + 团队标准销售额≥15万元",
"营业部部长:个人标准销售额≥799元 + 团队有效人数≥20人 + 团队标准销售额≥50万元"
], ],
"isActive": 1, "isActive": 1,
"isMore": true "isMore": true
...@@ -70,8 +68,6 @@ export default [ ...@@ -70,8 +68,6 @@ export default [
"A": [ "A": [
"见习合伙人:自购或分享产品,他人购买后可获得销售收入", "见习合伙人:自购或分享产品,他人购买后可获得销售收入",
"更高级别合伙人:可额外获得团队订单的一级/二级管理津贴", "更高级别合伙人:可额外获得团队订单的一级/二级管理津贴",
"营业部部长:可享受部长津贴",
"育成营业部部长:可享受育成津贴",
"(以上收益可叠加)" "(以上收益可叠加)"
], ],
"isActive": 1, "isActive": 1,
......
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