Commit 115c9a77 by Sweet Zhang

增加学习研讨模块

parent a46b8865
<template>
<view class="pdf-container">
<!-- 顶部导航 -->
<view class="pdf-header">
<button class="back-btn" @click="onBack">
<uni-icons type="back" size="28" color="#333"></uni-icons>
</button>
<text class="pdf-title">{{ title }}</text>
<view class="empty-btn"></view> <!-- 占位,使标题居中 -->
</view>
<!-- PDF查看区域 -->
<view class="pdf-content">
<template v-if="isLoading">
<view class="loading">
<uni-loading-icon type="spin" size="40"></uni-loading-icon>
<text class="loading-text">加载中...</text>
</view>
</template>
<template v-else-if="errorMsg">
<view class="error">
<uni-icons type="error" size="40" color="#f53f3f"></uni-icons>
<text class="error-text">{{ errorMsg }}</text>
</view>
</template>
<template v-else>
<web-view
:src="pdfUrl"
@message="handleMessage"
class="web-view"
></web-view>
</template>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onLoad, onReady } from 'vue';
import { onBackPress } from '@dcloudio/uni-app';
// 接收参数
const title = ref('');
const url = ref('');
const isLoading = ref(true);
const errorMsg = ref('');
const pdfUrl = ref('');
// 页面加载时获取参数
onLoad((options: { title?: string; url?: string }) => {
if (options.title) {
title.value = decodeURIComponent(options.title);
}
if (options.url) {
url.value = decodeURIComponent(options.url);
loadPdf();
} else {
errorMsg.value = '未找到PDF文件';
isLoading.value = false;
}
});
// 加载PDF文件
const loadPdf = () => {
// 使用PDF.js viewer加载PDF,并禁用下载功能
// 注意:需将pdfjs的web/viewer.html放入项目的static目录
const viewerPath = '/static/pdfjs/web/viewer.html';
const fileUrl = `${viewerPath}?file=${encodeURIComponent(url.value)}&disableDownload=true`;
// 本地PDF需要转换为网络路径
if (url.value.startsWith('/static/')) {
// 转换为绝对路径
const absoluteUrl = `${location.origin}${url.value}`;
pdfUrl.value = `${viewerPath}?file=${encodeURIComponent(absoluteUrl)}&disableDownload=true`;
} else {
pdfUrl.value = fileUrl;
}
isLoading.value = false;
};
// 处理web-view消息
const handleMessage = (e: any) => {
const data = e.detail.data[0];
if (data.action === 'error') {
errorMsg.value = data.message || 'PDF加载失败';
isLoading.value = false;
}
};
// 返回按钮
const onBack = () => {
uni.navigateBack();
};
// 监听物理返回键
onBackPress(() => {
onBack();
return true;
});
</script>
<style scoped>
.pdf-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #000;
}
.pdf-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
background-color: #fff;
border-bottom: 1px solid #eee;
}
.back-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
background-color: transparent;
}
.pdf-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
flex: 1;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0 20rpx;
}
.empty-btn {
width: 60rpx;
height: 60rpx;
}
.pdf-content {
flex: 1;
position: relative;
}
.web-view {
width: 100%;
height: 100%;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
}
.loading-text {
color: #fff;
font-size: 28rpx;
margin-top: 20rpx;
}
.error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
padding: 0 40rpx;
text-align: center;
}
.error-text {
color: #fff;
font-size: 28rpx;
margin-top: 20rpx;
}
</style>
......@@ -49,7 +49,7 @@ const config = {
stage,
prod
}
let env = 'prod';
let env = 'dev';
let baseURL = config[env].base_url;
let apiURL = config[env].api_url;
......
......@@ -17,6 +17,7 @@
"js-sha256": "^0.11.1",
"mp-painter": "^1.0.1",
"nanoid": "^4.0.0",
"pdf.js": "^0.1.0",
"qrcode": "^1.5.4",
"qrcodejs2": "^0.0.2",
"uqrcodejs": "^4.0.7"
......@@ -83,6 +84,20 @@
"wrap-ansi": "^6.2.0"
}
},
"node_modules/coffee-script": {
"version": "1.12.7",
"resolved": "https://registry.npmmirror.com/coffee-script/-/coffee-script-1.12.7.tgz",
"integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==",
"deprecated": "CoffeeScript on NPM has moved to \"coffeescript\" (no hyphen)",
"license": "MIT",
"bin": {
"cake": "bin/cake",
"coffee": "bin/coffee"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
......@@ -422,6 +437,17 @@
"node": ">=8"
}
},
"node_modules/pdf.js": {
"version": "0.1.0",
"resolved": "https://registry.npmmirror.com/pdf.js/-/pdf.js-0.1.0.tgz",
"integrity": "sha512-yBBGSI2aYuXCEGY6qHpU/j09P2cjxcCe9E6Qu0W7F6wUYyVD4+vi1aBLbgRJsaxe/DOslqn6dc01RxAvHUhfaw==",
"dependencies": {
"coffee-script": "latest"
},
"engines": {
"node": ">= 0.4.1"
}
},
"node_modules/pify": {
"version": "4.0.1",
"resolved": "https://registry.npmmirror.com/pify/-/pify-4.0.1.tgz",
......
......@@ -24,6 +24,7 @@
"js-sha256": "^0.11.1",
"mp-painter": "^1.0.1",
"nanoid": "^4.0.0",
"pdf.js": "^0.1.0",
"qrcode": "^1.5.4",
"qrcodejs2": "^0.0.2",
"uqrcodejs": "^4.0.7"
......
......@@ -474,6 +474,34 @@
"topWindow": false,
"navigationBarTitleText": "批量数据计算"
}
},
{
"path" : "components/pdf-viewer/pdf-viewer",
"style" :
{
"navigationBarTitleText" : "查看"
}
},
{
"path" : "pages/personalCenter/detail",
"style" :
{
"navigationBarTitleText" : "",
"webview": {
// 1. 允许加载本地静态资源(viewer.html /static 目录)
"allowAccessFromFileURLs": true,
// 2. 允许加载外部资源(OSS的PDF)
"allowUniversalAccessFromFileURLs": true,
// 3. 启用JavaScript(PDF.js依赖)
"enableJavaScript": true,
// 4. 关闭安全校验(开发环境测试用,线上可关闭)
"disableWebSecurity": true,
// 5. 允许跳转外部链接
"allowNavigateToExternalPages": true,
// 6. 启用调试(关键:便于查看WebView内部错误)
"webviewDebug": true
}
}
}
],
......
<template>
<view class="module-container">
<!-- 内容区域 -->
<view class="content">
<!-- 加载状态 -->
<view v-if="loading" class="loading">
<view class="spinner"></view>
<text>加载中...</text>
</view>
<!-- 1. 公司介绍(直接显示单个PDF) -->
<view v-if="!loading && currentType === 1 && companyPdf.url" class="pdf-single">
<web-view
:src="getPdfViewerUrl(companyPdf.url)"
class="webview"
@error="handlePdfError"
></web-view>
</view>
<!-- 2-4. 带Tab的模块(根据type显示不同Tab) -->
<view v-if="!loading && currentType >= 2 && currentType <= 4">
<!-- Tab切换栏 -->
<view class="tab-bar">
<view
v-for="(tab, index) in currentTabs"
:key="tab.id"
class="tab-item"
:class="{ active: activeTab === index }"
@click="switchTab(index)"
>
<text class="tab-text">{{ tab.title }}</text>
<view class="tab-indicator" v-if="activeTab === index"></view>
</view>
</view>
<!-- Tab对应的PDF内容 -->
<!-- Tab 对应的 PDF 内容:添加 @load 事件 -->
<view class="pdf-tab-content">
<web-view
:src="getPdfViewerUrl(currentPdf.url)"
class="webview"
@error="handlePdfError"
@load="handleWebViewLoad"
:key="getPdfViewerUrl(currentPdf.url)"
></web-view>
</view>
</view>
<!-- 错误状态 -->
<view v-if="!loading && (currentType < 1 || currentType > 4)" class="error">
<text>无效的模块类型</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
// 定义PDF文件类型
interface PdfItem {
id: string;
title: string;
url: string; // PDF文件路径
}
// 接收type参数
const currentType = ref<number>(1);
const loading = ref(true);
const activeTab = ref(0);
const currentPdf = ref<PdfItem>({ id: '', title: '', url: '' });
// OSS基础路径(根据实际OSS地址修改)
const OSS_BASE_URL = 'https://csf-doc-center.oss-cn-shanghai-finance-1-pub.aliyuncs.com';
// 公司介绍PDF(type=1)- 使用OSS地址
const companyPdf = ref<PdfItem>({
id: 'company',
title: '公司介绍',
url: `${OSS_BASE_URL}/public/company-intro.pdf` // OSS地址
});
// 所有模块的Tab配置(根据type匹配)
const tabConfig = ref<Record<number, PdfItem[]>>({
2: [ // type=2:案例分享
{ id: 'case1', title: '规划案例', url: `${OSS_BASE_URL}/cases/planning.pdf` },
{ id: 'case2', title: '团财案例', url: `${OSS_BASE_URL}/cases/group-finance.pdf` }
],
3: [ // type=3:产品分析
{ id: 'product2', title: '港险分析', url: `${OSS_BASE_URL}/products/hk-analysis.pdf` },
{ id: 'product1', title: '港险明细', url: `${OSS_BASE_URL}/products/hk-detail.pdf` },
],
4: [ // type=4:制度
{ id: 'policy1', title: '个险政策', url: `${OSS_BASE_URL}/policies/individual-policy.pdf` },
{ id: 'policy2', title: '团财政策', url: `${OSS_BASE_URL}/policies/group-policy.pdf` },
{ id: 'policy3', title: '介绍费政策', url: `${OSS_BASE_URL}/policies/commission-policy.pdf` }
]
});
// 初始化页面
onLoad((query) => {
// 解析type参数(默认1)
currentType.value = Number(query.type) || 1;
loading.value = true;
// 初始化Tab和PDF
const tabs = tabConfig.value[currentType.value] || [];
if (currentType.value >= 2 && currentType.value <= 4 && tabs.length > 0) {
activeTab.value = 0;
currentPdf.value = tabs[0]; // 直接赋值,避免 computed 延迟
}
// 模拟加载延迟(可缩短到 200ms,减少 WebView 等待时间)
setTimeout(() => {
loading.value = false;
}, 200);
});
// 计算当前模块的Tab列表
const currentTabs = computed(() => {
return tabConfig.value[currentType.value] || [];
});
// 1. 生成随机字符串(比时间戳更唯一,避免快速切换重复)
const getRandomStr = (length = 8) => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let str = '';
for (let i = 0; i < length; i++) {
str += chars.charAt(Math.floor(Math.random() * chars.length));
}
return str;
};
// 2. 生成 PDF 查看器 URL:添加随机参数+时间戳,确保每次 src 唯一
const getPdfViewerUrl = (pdfUrl: string) => {
if (!pdfUrl) return '';
const randomStr = getRandomStr(); // 随机字符串
const timestamp = new Date().getTime(); // 时间戳
// 同时添加 random 和 timestamp,双重确保 src 唯一
return `/static/pdf/web/viewer.html?file=${encodeURIComponent(pdfUrl)}&random=${randomStr}&t=${timestamp}`;
};
// 新增:WebView 加载完成监听
const handleWebViewLoad = (e: any) => {
const loadedSrc = e.detail.src; // 获取 WebView 实际加载的 src
console.log('WebView 加载完成:', loadedSrc);
// 解析加载的 file 参数,确认是否正确
const urlObj = new URL(loadedSrc);
const fileParam = urlObj.searchParams.get('file');
if (fileParam) {
const decodedFileUrl = decodeURIComponent(fileParam);
console.log('解析的 PDF 地址:', decodedFileUrl);
// 验证 PDF 地址是否可访问(可选:用 uni.request 测试)
uni.request({
url: decodedFileUrl,
method: 'GET',
success: (res) => {
console.log('PDF 地址可访问,响应状态:', res.statusCode);
},
fail: (err) => {
console.error('PDF 地址访问失败:', err);
uni.showToast({ title: `PDF 地址无效: ${err.errMsg}`, icon: 'none' });
}
});
}
};
// // 切换Tab
// const switchTab = (index: number) => {
// if (index < 0 || index >= currentTabs.value.length) return;
// activeTab.value = index;
// currentPdf.value = currentTabs.value[index];
// };
// 修改 switchTab 方法,添加 100ms 延迟
const switchTab = (index: number) => {
if (index < 0 || index >= currentTabs.value.length) return;
// 先显示加载状态(可选,增强用户体验)
uni.showLoading({ title: '加载中...' });
// 延迟 100ms 再更新 activeTab 和 currentPdf,避免 WebView 加载冲突
setTimeout(() => {
activeTab.value = index;
currentPdf.value = currentTabs.value[index];
// 延迟关闭加载提示(给 WebView 启动时间)
setTimeout(() => {
uni.hideLoading();
}, 300);
}, 100);
};
// 处理PDF加载错误
const handlePdfError = () => {
uni.showToast({
title: 'PDF加载失败',
icon: 'none',
duration: 2000
});
};
</script>
<style scoped lang="scss">
.module-container {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f5f5f5;
}
.navbar {
display: flex;
align-items: center;
height: 44px;
padding: 0 16px;
background-color: #ffffff;
border-bottom: 1px solid #eeeeee;
.back-btn {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
padding: 0;
.icon-back {
font-size: 20px;
color: #333333;
}
}
.title {
flex: 1;
text-align: center;
font-size: 17px;
font-weight: 500;
color: #333333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.content {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
}
/* 加载状态 */
.loading {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #ffffff;
.spinner {
width: 30px;
height: 30px;
border: 3px solid #eeeeee;
border-top-color: #20269B;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
text {
color: #999999;
font-size: 14px;
}
}
/* 错误状态 */
.error {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: #ef4444;
font-size: 16px;
background-color: #ffffff;
}
/* 单个PDF展示(公司介绍) */
.pdf-single {
flex: 1;
width: 100%;
height: 100%;
background-color: #ffffff;
}
/* Tab栏样式 */
.tab-bar {
display: flex;
flex-direction: row;
height: 48px;
background-color: #ffffff;
border-bottom: 1px solid #eeeeee;
overflow-x: auto;
white-space: nowrap;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
.tab-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 16px;
height: 100%;
position: relative;
.tab-text {
font-size: 14px;
color: #666666;
transition: color 0.3s;
}
.tab-indicator {
position: absolute;
bottom: 0;
width: 24px;
height: 2px;
background-color: #20269B;
border-radius: 1px;
}
&.active {
.tab-text {
color: #20269B;
font-weight: 500;
}
}
}
}
/* Tab对应的PDF内容 */
.pdf-tab-content {
width: 100%;
position: absolute;
top: 48px; /* 等于 Tab 栏的高度(.tab-bar { height: 48px; }),从 Tab 栏下方开始 */
bottom: 0; /* 到屏幕底部结束 */
background-color: #ffffff;
}
.pdf-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #999999;
font-size: 14px;
}
/* web-view样式 */
.webview {
width: 100%;
height: 100%;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
......@@ -218,6 +218,13 @@
{title:'我的团队',icon:'icon-tuandui',link:'/myPackageA/myTeam/myTeam',isOpen:true,isShow:true,identity: true},
{title:'育成团队',icon:'icon-yuchengguanxi',link:'/pages/personalCenter/myTeamIncubate',isOpen:true,isShow:true,identity: true},
],
},{id:'03',categoryName:'学习研讨',
children:[
{title:'公司介绍',icon:'icon-gongsi',link:'/pages/personalCenter/detail',isOpen:true,isShow:true,identity:true,type:1},
{title:'案例分享',icon:'icon-shiyongjiaocheng',link:'/pages/personalCenter/detail',isOpen:true,isShow:true,identity: true,type:2},
{title:'产品分析',icon:'icon-shujufenxi',link:'/pages/personalCenter/detail',isOpen:true,isShow:true,identity: true,type:3},
{title:'制度',icon:'icon-xiaoshoue',link:'/pages/personalCenter/detail',isOpen:true,isShow:true,identity: true,type:4},
],
},
{id:'02',categoryName:'帮助',
children:[
......@@ -287,7 +294,7 @@
// #ifdef H5
const systemInfo = uni.getSystemInfoSync();
this.shareBtnBottom = systemInfo.platform === 'ios'
console.log('this.shareBtnBottom',this.shareBtnBottom);
// console.log('this.shareBtnBottom',this.shareBtnBottom);
initJssdkShare(() => {
setWechatShare();
}, window.location.href);
......@@ -559,7 +566,12 @@
});
}else{
const urlObj = JSON.parse(JSON.stringify(item))
urlObj.link = `${urlObj.link}?from=personalCenter&partnerType=${this.customerBasicInfo.partnerType}`
if(urlObj.type){
urlObj.link = `${urlObj.link}?type=${urlObj.type}&from=personalCenter&partnerType=${this.customerBasicInfo.partnerType}`
}else{
urlObj.link = `${urlObj.link}?from=personalCenter&partnerType=${this.customerBasicInfo.partnerType}`
}
if(item.isTab){
uni.switchTab({
url:`../../${urlObj.link}`
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed. Click to expand it.
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