Commit a179a568 by yuzhenWang

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

Dev

See merge request !81
parents a5333080 de2a5c14
......@@ -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"
/>
<!-- 添加一个滚动容器 -->
<scroll-view
class="pdf-scroll-view"
scroll-y
:show-scrollbar="false"
@scroll="handleScroll"
:scroll-top="scrollTop"
>
<!-- PDF文档信息 -->
<view class="pdf-info" v-if="pdfInfo.title">
<view class="pdf-viewer" :style="{ height: containerHeight + 'px' }">
<!-- 标题 -->
<view v-if="pdfInfo.title" class="pdf-header">
<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}`"
<!-- 滚动容器 -->
<scroll-view
class="pdf-scroll"
scroll-y
:show-scrollbar="false"
@scroll="onScroll"
:scroll-top="scrollTop"
:style="{ height: scrollContainerHeight + '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-for="pageNum in totalPages"
:key="pageNum"
class="page-item"
:style="{ minHeight: getPageMinHeight(pageNum) + 'px' }"
>
<!-- 页码标签 -->
<view v-if="props.showPageNumber" class="page-number-tag">
{{ pageNum }}
</view>
<!-- 缩小模式:widthFix + 固定最大高度防过长 -->
<view v-if="!isZoomed[pageNum] && hasImage(pageNum)" class="fit-mode">
<image
:src="getImage(pageNum)"
mode="widthFix"
class="pdf-image-fit"
:show-menu-by-longpress="false"
/>
</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-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 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="globalLoading" class="global-status">
<view class="spinner"></view>
<text>正在加载文档...</text>
</view>
<view class="page-content">
<view class="loadEffect" v-if="!getPageImage(pageIndex) || isPageLoading(pageIndex)"></view>
<image
v-if="getPageImage(pageIndex)"
:src="getPageImage(pageIndex)"
mode="widthFix"
class="pdf-image"
@load="handlePageImageLoad(pageIndex)"
@error="handlePageImageError(pageIndex)"
: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="globalError" class="global-status error">
<text>{{ errorMessage }}</text>
<button size="mini" @click="reload">重试</button>
</view>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading-state">
<view class="loading-spinner"></view>
<text>文件较大,正在加载中...</text>
</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>
<!-- 加载进度 -->
<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-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>
</scroll-view>
</scroll-view>
</view>
</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 onOrientationChange = (orientation: 'portrait' | 'landscape') => {
console.log('屏幕方向变为:', orientation);
};
const lastScroll = ref(0);
const queue = ref<number[]>([]);
const isProcessingQueue = ref(false);
// 全局状态
const globalLoading = ref(false);
const globalError = ref(false);
const errorMessage = ref('');
// ========================== 计算属性 ==========================
const loadedPages = computed(() => {
return loadedPageSet.value.size;
});
// ================== COMPUTED ==================
const progress = computed(() =>
totalPages.value > 0 ? (loadedSet.value.size / totalPages.value) * 100 : 0
);
const hasMoreToLoad = computed(() => {
return loadedPages.value < pdfPageCount.value;
});
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;
// ========================== 生命周期 ==========================
// ================== 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();
// ================== LOAD DOCUMENT ==================
const loadDocument = async () => {
// ====== 平台差异化设置 worker ======
let useWorkerFlag = false;
// #ifdef H5
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker; // ← 静态导入
useWorkerFlag = true;
// #endif
const doc = await pdfjsLib.getDocument({
url: props.pdfInfo.url,
cMapUrl: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.11.338/cmaps/',
cMapPacked: true,
useWorker: useWorkerFlag,
}).promise;
pdfDoc.value = doc;
totalPages.value = doc.numPages;
// 初始化数组
images.value = new Array(doc.numPages).fill('');
loading.value = new Array(doc.numPages).fill(false);
};
// ================== PAGE LOADING ==================
const preloadInitialPages = () => {
const count = Math.min(3, totalPages.value);
for (let i = 1; i <= count; i++) addToQueue(i);
processQueue();
};
// ========================== 内部方法 ==========================
/**
* 重置状态
*/
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 addToQueue = (pageNum: number) => {
if (!queue.value.includes(pageNum)) queue.value.push(pageNum);
};
/**
* 清理资源
*/
const cleanup = () => {
if (pdfDoc.value) {
pdfDoc.value.destroy();
pdfDoc.value = null;
const processQueue = async () => {
if (isProcessingQueue.value || queue.value.length === 0) return;
isProcessingQueue.value = true;
while (queue.value.length > 0) {
const pageNum = queue.value.shift()!;
await loadPage(pageNum);
await new Promise(r => setTimeout(r, 10));
}
};
/**
* 设置错误状态
*/
const setError = (message: string) => {
error.value = true;
errorMessage.value = message;
loading.value = false;
isProcessingQueue.value = false;
};
/**
* 加载PDF文档
*/
const loadPdfDocument = async (): Promise<number> => {
const loadPage = async (pageNum: number) => {
if (!isMounted.value || !pdfDoc.value || loadedSet.value.has(pageNum)) return;
if (getRetryCount(pageNum) >= props.maxRetryCount) return;
try {
const loadingTask = 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,
});
pdfDoc.value = await loadingTask.promise;
pdfPageCount.value = pdfDoc.value.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;
} catch (err: any) {
console.error('PDF文档加载失败:', err);
throw new Error(`文档加载失败: ${err.message}`);
loading.value[pageNum - 1] = true;
const page = await pdfDoc.value.getPage(pageNum);
// 获取原始尺寸
const originalViewport = page.getViewport({ scale: 1 });
pageOriginalWidth.value[pageNum] = originalViewport.width;
pageOriginalHeight.value[pageNum] = originalViewport.height;
// 计算高清渲染尺寸(目标宽度 ~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;
// 存储渲染结果
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;
}
};
/**
* 滚动处理 - 使用 scroll-view 的 scroll 事件
*/
const handleScroll = (e: any) => {
if (!props.lazyLoad || loading.value) return;
const now = Date.now();
if (now - lastScrollTime.value < scrollThrottle.value) {
return;
}
lastScrollTime.value = now;
// 使用防抖
clearTimeout((window as any).scrollTimer);
(window as any).scrollTimer = setTimeout(() => {
checkVisiblePages(e.detail.scrollTop);
}, 100);
// ================== ZOOM MODE ==================
const toggleZoom = (pageNum: number) => {
if (!hasImage(pageNum)) return;
isZoomed.value[pageNum] = true;
};
/**
* 检查可见页面 - 修改为接收 scrollTop 参数
*/
const checkVisiblePages = (scrollTop: number) => {
if (pdfPageCount.value === 0 || loadingQueue.value.length > 5) return;
console.log('开始检查可见页面...', scrollTop);
const windowHeight = uni.getSystemInfoSync().windowHeight;
// 计算可见区域
const visibleTop = scrollTop - 500; // 提前500px开始加载
const visibleBottom = scrollTop + windowHeight + 1000; // 延后1000px加载
// 检查每个页面是否在可见区域内
for (let i = 1; i <= pdfPageCount.value; i++) {
// 如果页面已经加载或正在加载,跳过
if (loadedPageSet.value.has(i) || isPageLoading(i)) {
continue;
}
// 检查页面位置
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);
}
// 更新当前页
if (rect.top < windowHeight / 2 && rect.bottom > windowHeight / 2) {
if (currentPage.value !== i) {
currentPage.value = i;
emit('pageChange', i, pdfPageCount.value);
}
}
}
})
.exec();
}
// 处理加载队列
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]}`);
for (const pageNumber of pagesToLoad) {
await loadPdfPage(pageNumber);
}
} catch (err) {
console.error('处理加载队列失败:', err);
} finally {
isProcessingQueue.value = false;
// 如果队列中还有任务,继续处理
if (loadingQueue.value.length > 0) {
setTimeout(processLoadingQueue, 200);
} else {
// 队列处理完成后,再次检查可见页面
setTimeout(() => {
checkVisiblePages();
}, 300);
}
}
const currentX = touches[0].clientX - start.x;
const currentY = touches[0].clientY - start.y;
const sys = uni.getSystemInfoSync();
const viewW = sys.windowWidth;
const viewH = sys.windowHeight;
const imgW = pageRenderWidth.value[pageNum] || 0;
const imgH = pageRenderHeight.value[pageNum] || 0;
if (imgW === 0 || imgH === 0) return;
const minX = viewW - imgW > 0 ? 0 : viewW - imgW;
const minY = viewH - imgH > 0 ? 0 : viewH - imgH;
const maxX = 0;
const maxY = 0;
translateX.value[pageNum] = Math.min(maxX, Math.max(minX, currentX));
translateY.value[pageNum] = Math.min(maxY, Math.max(minY, currentY));
};
/**
* 加载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++;
console.log(`开始加载页面 ${pageNumber}...`);
if (!pdfDoc.value) {
throw new Error('PDF文档未加载');
}
const page = await pdfDoc.value.getPage(pageNumber);
// 根据设备像素比动态设置缩放
const pixelRatio = window.devicePixelRatio || 1;
const scale = Math.max(1.5, pixelRatio); // 至少 1.5 倍,高分屏自动更高
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(',')}`);
// 清理
canvas.width = 0;
canvas.height = 0;
// 清除失败记录
if (failedPages.value[pageNumber]) {
delete failedPages.value[pageNumber];
}
} catch (err: any) {
console.error(`页面 ${pageNumber} 加载失败:`, err);
// 记录失败次数
if (!failedPages.value[pageNumber]) {
failedPages.value[pageNumber] = 1;
} else {
failedPages.value[pageNumber]++;
}
// 对有问题的页面生成占位图
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);
}
} finally {
imgLoading.value[pageNumber - 1] = false;
currentLoading.value--;
}
const onTouchEnd = () => {
delete (window as any).pdfTouchStart;
};
/**
* 手动加载下一页
*/
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;
}
}
// ================== 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 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];
const checkVisible = () => {
if (totalPages.value === 0) return;
const sys = uni.getSystemInfoSync();
const winHeight = sys.windowHeight;
const top = scrollTop.value;
// 👇 关键:不依赖页面高度,直接按页码区间预加载
// 假设每页至少占 200px(保守值)
const MIN_PAGE_HEIGHT = 200; // px
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)
);
// 预加载前后各 2 页(共约 5~7 页)
const startPage = Math.max(1, visibleStartPage - 2);
const endPage = Math.min(totalPages.value, visibleEndPage + 2);
for (let i = startPage; i <= endPage; i++) {
if (!loadedSet.value.has(i) && !isLoading(i) && !isFailed(i)) {
addToQueue(i);
}
}
// 从已加载集合中移除
loadedPageSet.value.delete(pageNumber);
pdfImages.value[pageNumber - 1] = '';
addToLoadingQueue(pageNumber);
processLoadingQueue();
};
// ========================== 辅助方法 ==========================
const getPageImage = (pageIndex: number): string => {
return pdfImages.value[pageIndex - 1] || '';
if (queue.value.length > 0 && !isProcessingQueue.value) {
processQueue();
}
};
const isPageLoading = (pageIndex: number): boolean => {
return imgLoading.value[pageIndex - 1] || false;
// ================== UTILS ==================
const retryPage = (pageNum: number) => {
if (getRetryCount(pageNum) < props.maxRetryCount) {
addToQueue(pageNum);
processQueue();
}
};
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-size: 32rpx;
font-weight: bold;
}
.page-container {
margin-bottom: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
.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-status {
font-size: 24rpx;
&.success {
color: #52c41a;
}
&.error {
color: #ff4d4f;
}
}
.pdf-scroll {
width: 100%;
}
.page-content {
.page-item {
position: relative;
min-height: 400rpx;
.pdf-image {
width: 100%;
height: auto;
display: block;
// 禁用长按菜单
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
// 禁止长按保存
pointer-events: none;
}
margin-bottom: 10rpx;
padding: 0 24rpx;
}
.loadEffect {
width: 200rpx;
height: 200rpx;
.page-number-tag {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: url('../../static/range-fullloading/loading.gif') no-repeat center;
background-size: contain;
top: -40rpx;
left: 24rpx;
background: #20269B;
color: white;
padding: 4rpx 12rpx;
border-radius: 20rpx;
font-size: 24rpx;
z-index: 10;
}
.page-error {
.pdf-image-fit {
width: 100%;
display: block;
border-radius: 12rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
}
.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;
align-items: center;
background: #f9f9f9;
border-radius: 12rpx;
font-size: 28rpx;
color: #999;
}
.placeholder.error {
color: #ff4d4f;
.error-text {
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;
.zoom-container {
overflow: hidden;
background: #f9f9f9;
border-radius: 12rpx;
min-height: 400rpx;
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 0;
.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;
}
text {
color: #666;
font-size: 28rpx;
}
.image-original {
position: relative;
transition: transform 0.1s ease-out;
}
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 0;
text-align: center;
.error-icon {
font-size: 60rpx;
margin-bottom: 24rpx;
}
.error-message {
color: #ff4d4f;
font-size: 28rpx;
margin-bottom: 32rpx;
}
.retry-button {
background: #20269B;
color: white;
border: none;
padding: 16rpx 32rpx;
border-radius: 8rpx;
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;
}
.progress-info {
padding: 12rpx;
background: #f8f9fa;
.global-status {
text-align: center;
text {
display: block;
color: #666;
font-size: 26rpx;
margin-bottom: 16rpx;
padding: 80rpx 0;
color: #666;
.spinner {
margin: 0 auto 16rpx;
}
}
.global-status.error button {
margin-top: 20rpx;
background: #20269B;
color: white;
border: none;
}
.progress-bar {
width: 100%;
height: 8rpx;
background: #e8e8e8;
border-radius: 4rpx;
overflow: hidden;
.progress-inner {
height: 100%;
background: #20269B;
transition: width 0.3s ease;
padding: 20rpx;
text-align: center;
font-size: 26rpx;
color: #666;
.bar-bg {
width: 100%;
height: 8rpx;
background: #eee;
border-radius: 4rpx;
margin-top: 8rpx;
.bar-fill {
height: 100%;
background: #20269B;
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;
.action-btn {
flex: 1;
margin: 0 8rpx;
background: #20269B;
color: white;
border: none;
padding: 16rpx;
border-radius: 8rpx;
font-size: 26rpx;
&:disabled {
background: #ccc;
color: #999;
}
}
justify-content: center;
margin-top: 20rpx;
}
.zoom-btn, .reset-btn-inline {
padding: 8rpx 24rpx;
font-size: 24rpx;
border-radius: 8rpx;
background: #20269B;
color: white;
border: none;
line-height: 1;
}
.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();
......
......@@ -26,39 +26,37 @@
<!-- 对比内容(按group分组展示) -->
<!-- 对比内容:从 productPKInfoList.basicInfos 提取分组渲染 -->
<view class="compare-content" v-if="productPKInfoList.length > 0">
<!-- 核心:从第一个产品的 basicInfos 中遍历分组(A/B/C组) -->
<view class="compare-content" v-if="productPKInfoList.length > 0 && renderGroups.length > 0">
<view
class="compare-section"
v-for="group in renderGroups"
:key="group.groupCode"
>
<view class="section-header">
<uni-icons
:type="getGroupIcon(group.groupCode)"
size="22"
:color="getGroupColor(group.groupCode)"
></uni-icons>
<text class="section-title">{{ group.groupName }}</text>
</view>
<view class="section-content">
<view
class="compare-section"
v-for="group in getFirstProductGroups()"
:key="group.groupCode"
class="compare-item"
:class="{ 'hidden-item': shouldHide(field.fieldBizId) }"
v-for="field in group.fields"
:key="field.fieldBizId"
>
<view class="section-header">
<uni-icons
:type="getGroupIcon(group.groupCode)"
size="22"
:color="getGroupColor(group.groupCode)"
></uni-icons>
<text class="section-title">{{ group.groupName }}</text>
</view>
<view class="section-content">
<!-- 遍历当前分组下的所有属性(factor) -->
<view
class="compare-item"
:class="{ 'hidden-item': shouldHide(group.groupCode, factor.type) }"
v-for="factor in group.factors"
:key="factor.type"
>
<text class="item-label">{{ factor.typeName }}</text>
<view class="item-values">
<!-- 遍历所有产品,匹配当前分组+当前属性的内容 -->
<view class="item-value" v-for="product in productPKInfoList" :key="product.planBizId">
{{ getProductFactorValue(product, group.groupCode, factor.type) || '-' }}
</view>
</view>
<text class="item-label">{{ field.label }}</text>
<view class="item-values">
<view class="item-value" v-for="product in productPKInfoList" :key="product.planBizId">
{{ getProductFieldValue(product, field.fieldBizId) || '-' }}
</view>
</view>
</view>
</view>
</view>
<!-- 产品彩页分组(单独处理,从 planFiles 提取) -->
<view class="compare-section" id="product-file-group">
......@@ -96,6 +94,36 @@
<text class="empty-text">暂无对比产品数据</text>
</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>
<script setup>
......@@ -103,189 +131,162 @@ import { ref, computed, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import common from '@/common/common';
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 route = useRoute();
// 后端返回的原始数据
// 原始数据
const resultData = ref({});
// 配置分组列表(基本信息、适合人群等)
const configList = ref([]);
// 产品对比信息列表
const configList = ref([]); // ← 新增:用于定义页面结构
const productPKInfoList = ref([]);
// 是否只看不同
const showOnlyDiff = ref(false);
// 查看文件的URL
const viewFileUrl = ref()
const currentPdfUrl = 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 iconMap = {
'A': 'info', // 基本信息
'B': 'auth', // 适合人群
'C': 'star', // 产品特色
'D': 'image' // 产品彩页
};
const iconMap = { A: 'info', B: 'auth', C: 'star', D: 'image' };
return iconMap[groupCode] || 'help';
};
// 根据分组编码获取对应的颜色
const getGroupColor = (groupCode) => {
const colorMap = {
'A': '#20269B', // 基本信息-蓝色
'B': '#00cc66', // 适合人群-绿色
'C': '#ff9900', // 产品特色-橙色
'D': '#cc66ff' // 产品彩页-紫色
};
const colorMap = { A: '#20269B', B: '#00cc66', C: '#ff9900', D: '#cc66ff' };
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 shareTitle = `多款产品对比结果`;
// 3. 分享描述(突出对比价值)
const shareDesc = `包含核心参数对比,快速了解差异`;
// 4. 分享图标(建议用公司LOGO或产品相关图标,尺寸200x200px,HTTPS地址)
const shareIcon = `${window.location.origin}/static/mypoint_pic.png`;
let data = {
title: shareTitle,
desc:shareDesc,
link: shareLink, //分享链接
imgUrl: shareIcon, //图片c
}
//安卓机型获取当前页面路径
let url = window.location.href.split('#')[0];
//ios机型获取当前页面路径
let ua = navigator.userAgent.toLowerCase();
let isWeixin = ua.indexOf('micromessenger') !== -1;
if (isWeixin) {
let isiOS = /(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent); //ios终端
if (isiOS && window.sessionStorage.getItem('firstEntryUrl')) {
url = window.sessionStorage.getItem('firstEntryUrl').split('#')[0];
}
}
hshare(data, url)
}
const wxShare = (productIds, categoryId) => {
const shareLink = `${window.location.origin}/myPackageA/compare-result/compare-result?categoryId=${categoryId}&productIds=${productIds}`;
const shareTitle = `多款产品对比结果`;
const shareDesc = `包含核心参数对比,快速了解差异`;
const shareIcon = `${window.location.origin}/static/mypoint_pic.png`;
let data = { title: shareTitle, desc: shareDesc, link: shareLink, imgUrl: shareIcon };
let url = window.location.href.split('#')[0];
let ua = navigator.userAgent.toLowerCase();
let isWeixin = ua.indexOf('micromessenger') !== -1;
if (isWeixin) {
let isiOS = /(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent);
if (isiOS && window.sessionStorage.getItem('firstEntryUrl')) {
url = window.sessionStorage.getItem('firstEntryUrl').split('#')[0];
}
}
hshare(data, url);
};
const navigateToPKPage = () => {
uni.navigateBack();
};
const getUrl = (fileUrl) => {
if (!fileUrl) {
uni.showToast({ title: '暂无文档', icon: 'none' });
return;
}
// 区分环境:H5 端直接打开 URL,其他端用原下载逻辑
if (uni.getSystemInfoSync().uniPlatform === 'web') {
// H5 方案:用浏览器新窗口打开 PDF(依赖浏览器原生支持)
const opened = window.open(fileUrl, '_blank');
} else {
// 非 H5 端(小程序/APP):保留原下载+打开逻辑
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);
}
});
}
uni.showLoading({ title: '加载PDF中...' });
currentPdfUrl.value = fileUrl;
showPdfModal.value = true;
setTimeout(() => {
pdfPopupRef.value?.open?.();
uni.hideLoading();
}, 100);
};
// 【核心1:获取第一个产品的分组列表(A/B/C组)】
// 所有产品的分组结构一致,取第一个产品的 basicInfos 作为分组源
const getFirstProductGroups = () => {
if (productPKInfoList.value.length === 0) return [];
return productPKInfoList.value[0].basicInfos || [];
const closePdfModal = () => {
showPdfModal.value = false;
setTimeout(() => pdfPopupRef.value?.close?.(), 100);
};
// 【核心2:获取指定产品、指定分组、指定属性的内容】
const getProductFactorValue = (product, groupCode, factorType) => {
// 1. 找到产品中当前分组(如A组-基本信息)
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) => {
if (!e.show && showPdfModal.value) showPdfModal.value = false;
};
onBeforeUnmount(() => {
if (pdfPopupRef.value) pdfPopupRef.value.close();
});
// 【核心3:判断属性是否所有产品都相同(“只看不同”逻辑)】
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;
};
// 【✅ 关键修正6:初始化时同时处理 configList 和 productPKInfoList】
onMounted(() => {
const { productIds, categoryId } = route.query;
if (!productIds || !categoryId) {
uni.showToast({ title: '缺少对比参数', icon: 'none' });
return;
}
// 前往PK选择页
const navigateToPKPage = () => {
uni.navigateBack()
const params = {
category: categoryId,
planBizIdList: productIds.split(',')
};
};
let ua = navigator.userAgent.toLowerCase();
let isWeixin = ua.indexOf('micromessenger') !== -1;
// 初始化数据
onMounted(() => {
api.getProductPKInfo(params).then(res => {
if (res.success && res.data) {
// ✅ 同时保存 configList 和 product 列表
initConfigStructure(res.data.configList || []);
productPKInfoList.value = res.data.productPKInfoList || [];
const { productIds, categoryId} = route.query;
if (!productIds || !categoryId) {
uni.showToast({ title: '缺少对比参数', icon: 'none' });
return;
}
const params = {
category: categoryId,
planBizIdList: productIds.split(',')
};
let ua = navigator.userAgent.toLowerCase();
let isWeixin = ua.indexOf('micromessenger') !== -1;
api.getProductPKInfo(params).then(res => {
if (res.success && res.data?.productPKInfoList) {
productPKInfoList.value = res.data.productPKInfoList;
// 微信环境初始化分享(如有)
if (isWeixin) {
wxShare(productIds,categoryId)
}
} else {
common.errorDialog(1, res.message || '获取对比数据失败');
}
});
})
if (isWeixin) wxShare(productIds, categoryId);
} else {
common.errorDialog(1, res.message || '获取对比数据失败');
}
});
});
</script>
<style scoped>
......@@ -488,4 +489,38 @@ onMounted(() => {
align-items: 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>
\ No newline at end of file
......@@ -160,7 +160,7 @@ const fetchRateData = async () => {
try {
const params = {
planBizId:planBizId.value,
userId:localStorage.getItem('cffp_userId')
userId:uni.getStorageSync('cffp_userId') || ''
}
const response = await api.queryRate(params)
......@@ -193,7 +193,7 @@ const fetchRateData = async () => {
// 生命周期
onMounted(() => {
userId.value = localStorage.getItem('cffp_userId') || ''
userId.value = uni.getStorageSync('cffp_userId') || ''
fetchRateData()
})
</script>
......
......@@ -131,7 +131,7 @@
<button
class="compare-btn"
@click="navigateToPKPage"
:disabled="pkList.length < 2"
:disabled="pkList.length < 1"
>
去对比
</button>
......
......@@ -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'
});
......@@ -271,7 +272,6 @@ const queryUserInfoAndPermission = () => {
const initPdfAfterPermission = () => {
loading.value = true;
const filteredTabs = filteredCurrentTabs.value;
console.log(filteredTabs)
if (currentType.value >= 2 && currentType.value <= 4) {
if (filteredTabs.length > 0) {
activeTab.value = 0;
......@@ -310,7 +310,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 +318,7 @@ const switchTab = (index: number) => {
uni.setStorageSync('tabsIndex', index);
setTimeout(() => {
uni.hideLoading();
loading.value = false;
}, 300);
}, 100);
};
......
......@@ -11,9 +11,10 @@
<view class="headerTop">
<view class="" style="margin-right: 15rpx;">
<view class="myName" v-if="loginType == 'codelogin'">
{{showMyName || '点头像完善信息'}}
{{showMyName || '点头像完善信息'}}
<text v-if="customerBasicInfo.partnerType"
class="typePartner">{{customerBasicInfo.partnerType}}</text>
</view>
<view class="myName" v-if="loginType == 'visitor'">游客</view>
</view>
......@@ -190,7 +191,7 @@
currentPage: 'personalCenter',
customerBasicInfo: {},
loginornot: true,
tabBarPadding: 0,
tabBarPadding: 100,
settingItem: {
title: '系统设置',
icon: 'setting',
......@@ -233,6 +234,7 @@
},
],
},
{
id: '01',
categoryName: '团队',
......@@ -253,7 +255,6 @@
isShow: true,
identity: true
},
// {title:'我的团队',icon:'icon-tuandui',link:'/pages/personalCenter/myTeam',isOpen:true,isShow:true,identity: true},
{
title: '我的团队',
icon: 'icon-tuandui',
......@@ -422,25 +423,22 @@
});
if (uni.getStorageSync('cffp_userInfo')) {
this.userInfo = JSON.parse(uni.getStorageSync('cffp_userInfo'))
}
// 计算tabbar高度,避免tabbar遮挡页面底部内容
const sysInfo = uni.getSystemInfoSync();
this.tabBarPadding = 100; // 默认值,可根据你的设计调整
if (sysInfo.windowBottom) {
// H5 平台下,windowBottom 表示可视区域底部到屏幕底的距离(即 tabbar 高度 + 安全区域)
this.tabBarPadding = sysInfo.windowBottom;
} else {
// 兜底逻辑:根据平台和设备估算
if (sysInfo.platform === 'ios') {
// iPhone 底部安全区一般为 34px(全面屏)或 0(非全面屏)
const isIPhoneX = /iPhone X|iPhone 1[0-9]|iPhone [A-Z]/.test(sysInfo.model);
this.tabBarPadding += isIPhoneX ? 34 : 0;
}
// 如果是 iPad,横屏时底部安全区可能为 0,但 tabbar 仍存在
if (sysInfo.deviceType === 'tablet') {
this.tabBarPadding = 60; // iPad 常见 tabbar 高度
if (this.userInfo) {
const levelCode = this.userInfo.levelCode || '';
const dealerId = this.userInfo.dealerId || '';
// 2. 找到“申请加盟”菜单项(通过 key)
const team = this.mainMenuLists.find(item => item.id === '01');
if (!team) return;
const applyItem = team.children.find(child => child.key === 'applyFranchise');
if (!applyItem) return;
// 3. 更新标题和显示状态
applyItem.title = levelCode === '' ? '申请加盟' : '晋升目标';
applyItem.isShow = !dealerId && levelCode != 'P3'; // 没有 dealerId 才显示
}
}
// #ifdef H5
initJssdkShare(() => {
setWechatShare();
......
......@@ -23,10 +23,8 @@ export default [
"Q": "合伙人晋升条件",
"A": [
"见习合伙人:完成加盟申请",
"新锐合伙人:个人标准销售额≥799元",
"资深合伙人:个人标准销售额≥799元 + 团队有效人数≥5人 + 团队标准销售额≥5万元",
"精英合伙人:个人标准销售额≥799元 + 团队有效人数≥10人 + 团队标准销售额≥15万元",
"营业部部长:个人标准销售额≥799元 + 团队有效人数≥20人 + 团队标准销售额≥50万元"
"新锐合伙人:个人标准销售额≥198元",
"资深合伙人:个人标准销售额≥198元 + 团队有效人数≥5人 + 团队标准销售额≥5万元",
],
"isActive": 1,
"isMore": true
......@@ -70,8 +68,6 @@ export default [
"A": [
"见习合伙人:自购或分享产品,他人购买后可获得销售收入",
"更高级别合伙人:可额外获得团队订单的一级/二级管理津贴",
"营业部部长:可享受部长津贴",
"育成营业部部长:可享受育成津贴",
"(以上收益可叠加)"
],
"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