jwyu 7 bulan lalu
induk
melakukan
c9ca51305c

+ 2 - 0
package.json

@@ -12,6 +12,7 @@
   "dependencies": {
     "axios": "^1.7.2",
     "normalize.css": "^8.0.1",
+    "pdfjs-dist": "2.16.105",
     "tdesign-mobile-vue": "^1.2.3",
     "vue": "^3.4.21",
     "vue-router": "4"
@@ -25,6 +26,7 @@
     "unplugin-icons": "^0.19.0",
     "unplugin-vue-components": "^0.27.0",
     "vite": "^5.2.0",
+    "vite-plugin-compression": "^0.5.1",
     "vite-plugin-svg-icons": "^2.0.1"
   }
 }

+ 61 - 0
pnpm-lock.yaml

@@ -7,6 +7,9 @@ dependencies:
   normalize.css:
     specifier: ^8.0.1
     version: 8.0.1
+  pdfjs-dist:
+    specifier: 2.16.105
+    version: 2.16.105
   tdesign-mobile-vue:
     specifier: ^1.2.3
     version: 1.2.3(vue@3.4.21)
@@ -42,6 +45,9 @@ devDependencies:
   vite:
     specifier: ^5.2.0
     version: 5.2.0(sass@1.77.4)
+  vite-plugin-compression:
+    specifier: ^0.5.1
+    version: 0.5.1(vite@5.2.0)
   vite-plugin-svg-icons:
     specifier: ^2.0.1
     version: 2.0.1(vite@5.2.0)
@@ -952,6 +958,14 @@ packages:
       supports-color: 2.0.0
     dev: true
 
+  /chalk@4.1.2:
+    resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+    engines: {node: '>=10'}
+    dependencies:
+      ansi-styles: 4.3.0
+      supports-color: 7.2.0
+    dev: true
+
   /chokidar@3.6.0:
     resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
     engines: {node: '>= 8.10.0'}
@@ -1245,6 +1259,11 @@ packages:
       domelementtype: 2.3.0
     dev: true
 
+  /dommatrix@1.0.3:
+    resolution: {integrity: sha512-l32Xp/TLgWb8ReqbVJAFIvXmY7go4nTxxlWiAFyhoQw9RKEOHBZNnyGvJWqDVSPmq3Y9HlM4npqF/T6VMOXhww==}
+    deprecated: dommatrix is no longer maintained. Please use @thednp/dommatrix.
+    dev: false
+
   /domutils@1.7.0:
     resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==}
     dependencies:
@@ -1713,6 +1732,11 @@ packages:
     engines: {node: '>=0.10.0'}
     dev: true
 
+  /has-flag@4.0.0:
+    resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+    engines: {node: '>=8'}
+    dev: true
+
   /has-property-descriptors@1.0.2:
     resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
     dependencies:
@@ -2430,6 +2454,18 @@ packages:
     resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
     dev: true
 
+  /pdfjs-dist@2.16.105:
+    resolution: {integrity: sha512-J4dn41spsAwUxCpEoVf6GVoz908IAA3mYiLmNxg8J9kfRXc2jxpbUepcP0ocp0alVNLFthTAM8DZ1RaHh8sU0A==}
+    peerDependencies:
+      worker-loader: ^3.0.8
+    peerDependenciesMeta:
+      worker-loader:
+        optional: true
+    dependencies:
+      dommatrix: 1.0.3
+      web-streams-polyfill: 3.3.3
+    dev: false
+
   /picocolors@1.0.1:
     resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
 
@@ -2999,6 +3035,13 @@ packages:
       has-flag: 1.0.0
     dev: true
 
+  /supports-color@7.2.0:
+    resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+    engines: {node: '>=8'}
+    dependencies:
+      has-flag: 4.0.0
+    dev: true
+
   /supports-preserve-symlinks-flag@1.0.0:
     resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
     engines: {node: '>= 0.4'}
@@ -3363,6 +3406,19 @@ packages:
     engines: {node: '>= 0.8'}
     dev: true
 
+  /vite-plugin-compression@0.5.1(vite@5.2.0):
+    resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==}
+    peerDependencies:
+      vite: '>=2.0.0'
+    dependencies:
+      chalk: 4.1.2
+      debug: 4.3.5
+      fs-extra: 10.1.0
+      vite: 5.2.0(sass@1.77.4)
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
   /vite-plugin-svg-icons@2.0.1(vite@5.2.0):
     resolution: {integrity: sha512-6ktD+DhV6Rz3VtedYvBKKVA2eXF+sAQVaKkKLDSqGUfnhqXl3bj5PPkVTl3VexfTuZy66PmINi8Q6eFnVfRUmA==}
     peerDependencies:
@@ -3455,6 +3511,11 @@ packages:
       '@vue/server-renderer': 3.4.21(vue@3.4.21)
       '@vue/shared': 3.4.21
 
+  /web-streams-polyfill@3.3.3:
+    resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
+    engines: {node: '>= 8'}
+    dev: false
+
   /webpack-sources@3.2.3:
     resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==}
     engines: {node: '>=10.13.0'}

+ 14 - 1
src/api/modules/report.js

@@ -12,5 +12,18 @@ export default{
   // 取消收藏报告
   reportCollectCancel:params=>{
     return post('/myreport/collectCancel',params)
-  }
+  },
+
+  // 获取PDF报告详情
+  getPdfReportDetail:params=>{
+    return get('/report/pdf/detail',params)
+  },
+  // 收藏PDF报告
+  pdfReportCollect:params=>{
+    return post('/myreport/pdf/collect',params)
+  },
+  // 取消收藏PDF报告
+  pdfReportCollectCancel:params=>{
+    return post('/myreport/pdf/collectCancel',params)
+  },
 }

+ 136 - 0
src/components/PreviewPDF.vue

@@ -0,0 +1,136 @@
+<script setup>
+
+const props = defineProps({
+  url: {
+    require: true,
+    default: ''
+  }
+})
+
+import * as pdfjsLib from 'pdfjs-dist';
+import "pdfjs-dist/web/pdf_viewer.css";
+import workerUrl from 'pdfjs-dist/build/pdf.worker.min?url'
+//此处workerSrc只能是文件路径 故上面引入后缀一定要加 ?url 详见vite 静态资源处理模块
+pdfjsLib.GlobalWorkerOptions.workerSrc = workerUrl;
+import { Toast } from 'tdesign-mobile-vue';
+
+let pdfDoc = null
+let currentPage = 1
+const pageSize=5
+const pages = ref([])// 初始化加载第一页
+const loadingPages = ref([])
+
+async function loadPdf() {
+  const url = props.url; // 替换为你的 PDF 文件路径
+  const loadingTask = pdfjsLib.getDocument({
+    url:url,
+    disableAutoFetch: true, // 禁用自动获取
+    disableStream:true
+  })
+  try {
+    pdfDoc = await loadingTask.promise;
+    // console.log('pdfDoc',pdfDoc);
+    batchLoadPage();
+  } catch (error) {
+    console.error("Error loading PDF:", error);
+    Toast('PDF初始化失败请重试');
+  }
+}
+
+async function loadPage(pageNum) {
+  if (pdfDoc) {
+    loadingPages.value.push(pageNum)
+    try {
+      const page = await pdfDoc.getPage(pageNum);
+      const viewport = page.getViewport({ scale: 1 });
+      const canvas = document.getElementById(`pdfCanvas${pageNum-1}`)
+      const context = canvas.getContext("2d");
+
+      canvas.height = viewport.height;
+      canvas.width = viewport.width;
+
+      const renderContext = {
+        canvasContext: context,
+        viewport: viewport,
+      };
+
+      await page.render(renderContext).promise;
+      loadingPages.value = loadingPages.value.filter(p => p !== pageNum);
+    } catch (error) {
+      console.error("Error loading page:", error);
+      Toast(`第${pageNum}页加载请重试`);
+    }
+  }
+}
+
+function batchLoadPage(){
+  const startIndex=(currentPage-1)*pageSize+1
+  const endIndex=currentPage*pageSize>pdfDoc.numPages?pdfDoc.numPages:currentPage*pageSize
+  for (let index = startIndex; index <= endIndex; index++) {
+    // console.log(index);
+    pages.value.push(index);
+    loadPage(index)
+  }
+}
+
+function loadNextPage() {
+  if (currentPage < pdfDoc.numPages) {
+    currentPage++;
+    pages.value.push(currentPage);
+    loadPage(currentPage);
+  }
+}
+function onScroll(scrollBottom){
+  if(currentPage*pageSize>=pdfDoc.numPages) return
+  if (scrollBottom < 50) {
+    currentPage++;
+    batchLoadPage()
+  }
+}
+
+onMounted(() => {
+  loadPdf()
+})
+
+
+</script>
+
+<template>
+  <div id="pdfContainer" class="pdf-container">
+    <t-list @scroll="onScroll">
+    <div v-for="(page, index) in pages" :key="index" class="pdf-page">
+      <canvas :id="'pdfCanvas' + index"></canvas>
+      <div class="loading-box">
+        <t-loading theme="spinner" :loading="loadingPages.includes(index+1)" />
+      </div>
+    </div>
+    </t-list>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.pdf-container {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.pdf-page {
+  margin-bottom: 10px;
+  position: relative;
+}
+
+canvas {
+  display: block;
+  width: 100%;
+  height: auto;
+}
+
+.loading-box {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+}
+</style>

+ 8 - 0
src/router/modules/report.js

@@ -6,5 +6,13 @@ export default[
     meta:{
       title:'报告详情'
     },
+  },
+  {
+    path:'/report/pdf',
+    component:()=>import('@/views/report/PDF.vue'),
+    name:'ReportPDFDetail',
+    meta:{
+      title:'报告详情'
+    },
   }
 ]

+ 333 - 0
src/views/report/PDF.vue

@@ -0,0 +1,333 @@
+<script setup>
+import apiReport from '@/api/modules/report'
+import { useRoute } from 'vue-router'
+import { Message } from 'tdesign-mobile-vue';
+import apiCommon from '@/api/modules/common'
+import apiUser from '@/api/modules/user'
+
+const route = useRoute()
+
+// 获取系统配置
+let systemConfig = null
+function getSystemConfig() {
+  apiCommon.systemConfig().then(res => {
+    if (res.Ret === 200) {
+      systemConfig = res.Data
+      console.log(res.Data);
+    }
+  })
+}
+getSystemConfig()
+
+// 获取用户信息
+let userInfo = null
+async function getUserInfo() {
+  const res = await apiUser.userInfo()
+  if (res.Ret === 200) {
+    userInfo = res.Data
+  }
+}
+
+
+
+const reportId = route.query.reportid
+const reportInfo = ref(null)
+const reportContent = ref('')
+const reportStatus = ref(0)//1已过期,2没有该品种权限,3没有权限,4有权限,5未绑定
+const reportCollected = ref(false)//报告是否收藏
+const isBind=ref(false)
+async function getReportInfo() {
+  if (!reportId) return
+  const res = await apiReport.getPdfReportDetail({
+    ReportPdfId: Number(reportId)
+  })
+  if (res.Ret === 200) {
+    reportInfo.value = res.Data.Report
+    reportStatus.value = res.Data.Status
+    reportCollected.value = res.Data.IsCollect || false
+    isBind.value=res.Data.IsSignIn
+
+    // 设置分享文案
+    wx.miniProgram.postMessage({
+      data: {
+        title: res.Data.Report.Title
+      }
+    });
+  }
+}
+getReportInfo()
+
+
+// 拨打电话
+function handleCallPhone() {
+  let tel = userInfo.SellerPhone
+  if (!tel) {
+    systemConfig.forEach(item => {
+      if (item.ConfKey === 'ServicePhone') {
+        tel = item.ConfVal
+      }
+    });
+  }
+
+  var phoneLink = 'tel:' + tel;
+  var link = document.createElement('a');
+  link.setAttribute('href', phoneLink);
+  link.onclick = function () {
+    return true;
+  };
+  link.click();
+}
+
+// 点击收藏
+async function handleCollect() {
+  const res = reportCollected.value ? await apiReport.pdfReportCollectCancel({ ReportPdfId: Number(reportId) }) : await apiReport.pdfReportCollect({ ReportPdfId: Number(reportId) })
+  if (res.Ret === 200) {
+    Message.success(reportCollected.value ? '取消收藏成功' : '收藏成功')
+    reportCollected.value = !reportCollected.value
+    // 通知更新收藏列表
+    wx.miniProgram.postMessage({
+      data: 'refreshCollectList'
+    });
+  }
+}
+
+// 显示免责声明
+const isShowMZSM = ref(false)
+
+// 显示返回顶部
+const showToTop = ref(false)
+function handlePageScroll() {
+  const top = document.documentElement.scrollTop || document.body.scrollTop
+  if (top > window.outerHeight) {
+    showToTop.value = true
+  } else {
+    showToTop.value = false
+  }
+}
+function handleBackTop() {
+  document.body.scrollTop = document.documentElement.scrollTop = 0
+}
+
+onMounted(() => {
+  window.addEventListener('scroll', handlePageScroll)
+})
+onUnmounted(() => {
+  window.removeEventListener('scroll', handlePageScroll)
+})
+
+function handleGoLogin(){
+  const redirectUrl=encodeURIComponent(`/pages-report/reportDetail/index?id=${route.query.reportid}`) 
+  wx.miniProgram.reLaunch({
+    url:`/pages/login/index?redirectUrl=${redirectUrl}`
+  })
+}
+
+</script>
+
+<template>
+  <div :class="['report-detail-page',reportStatus !== 4||(reportStatus === 5&&!isBind)?'report-detail_hidden':'']" v-if="reportInfo">
+    <div class="title-box">{{ reportInfo.Title }}</div>
+    <div class="author-box">{{ reportInfo.Author }}</div>
+    <div class="time-box">
+      <span>{{ reportInfo.PublishTime }}</span>
+      <span class="btn" @click="isShowMZSM = true">免责声明</span>
+    </div>
+    <div class="des-box" v-if="reportInfo.Abstract">
+      <svg-icon name="icon01"></svg-icon>
+      <div>{{ reportInfo.Abstract }}</div>
+    </div>
+    <div class="report-content-box rich-content">
+      <preview-PDF :url="reportInfo.PdfUrl"/>
+    </div>
+    <!-- 右侧悬浮操作栏 -->
+    <div class="right-fix-box">
+      <!-- 收藏 -->
+      <svg-icon
+        @click="handleCollect"
+        class="item collect-icon"
+        :name="reportCollected ? 'collected' : 'collect'"
+        v-if="reportStatus === 4"
+      />
+      <!-- 返回顶部 -->
+      <div class="item back-top-img">
+        <svg-icon
+          name="backtop"
+          v-show="showToTop"
+          @click="handleBackTop"
+          class="back-top-img"
+        />
+      </div>
+    </div>
+  </div>
+  <!-- 无权限  -->
+  <div class="no-auth-wrap" v-if="reportStatus !== 4">
+    <div class="opcity-box"></div>
+    <div class="content-box">
+      <img class="icon" src="@/assets/imgs/lock-img.png" alt="" />
+      <div class="text" v-if="reportStatus === 3">
+        您暂无权限查看,<br />请联系客服人员开通!
+      </div>
+      <div class="text" v-if="reportStatus === 2">
+        您暂无该品种权限,<br />请联系销售人员开通!
+      </div>
+      <div class="text" v-if="reportStatus === 1">
+        您的权限已过期,<br />请联系销售人员开通!
+      </div>
+      <t-button
+        theme="primary"
+        block
+        style="width: 300px; margin: 30px auto"
+        @click="handleCallPhone"
+        >立即联系</t-button
+      >
+    </div>
+  </div>
+  <!-- 未绑定 -->
+  <div class="no-auth-wrap" v-if="reportStatus === 5&&!isBind">
+    <div class="opcity-box"></div>
+    <div class="content-box">
+      <img class="icon" src="@/assets/imgs/lock-img.png" alt="" />
+      <div class="text">
+        为了优化您的用户体验<br />请登录后查看更多信息!
+      </div>
+      <t-button
+        theme="primary"
+        block
+        style="width: 300px; margin: 30px auto"
+        @click="handleGoLogin"
+        >去登陆</t-button
+      >
+    </div>
+  </div>
+  <!-- 免责声明 -->
+  <disclaimers-wrap v-model:show="isShowMZSM" />
+</template>
+
+<style lang="scss" scoped>
+.report-detail-page {
+  background-color: #fff;
+  padding: var(--page-padding);
+  .title-box {
+    font-size: 36px;
+    line-height: 44px;
+    margin-bottom: 20px;
+  }
+  .time-box {
+    margin-top: 10px;
+    font-size: var(--font-size-small);
+    color: var(--text-color-grey);
+    .btn {
+      float: right;
+      color: var(--primary-color);
+    }
+  }
+  .des-box {
+    background-color: #f8f8f8;
+    padding: 20px;
+    margin: 20px 0;
+    display: flex;
+    gap: 0 10px;
+    color: var(--text-color-sub);
+    font-size: var(--font-size-small);
+    line-height: 36px;
+  }
+  .right-fix-box {
+    position: fixed;
+    z-index: 99;
+    right: 34px;
+    bottom: 130px;
+    .item {
+      margin-top: 10px;
+    }
+    .back-top-img {
+      width: 100px;
+      height: 100px;
+      display: block;
+    }
+    .collect-icon {
+      width: 100px;
+      height: 100px;
+      display: block;
+    }
+  }
+}
+.no-auth-wrap {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 99;
+  .opcity-box {
+    height: 129px;
+    background: linear-gradient(
+      360deg,
+      #ffffff 0%,
+      rgba(255, 255, 255, 0) 100%
+    );
+  }
+  .content-box {
+    background-color: #fff;
+    padding-bottom: 200px;
+    text-align: center;
+    color: var(--primary-color);
+  }
+
+  .icon {
+    display: block;
+    margin: 0 auto;
+    width: 200px;
+    height: 200px;
+  }
+}
+
+@media (min-width: 600px) {
+  .report-detail-page {
+    .title-box {
+      font-size: 18px;
+      line-height: 22px;
+      margin-bottom: 10px;
+    }
+    .time-box {
+      margin-top: 5px;
+    }
+    .des-box {
+      padding: 10px;
+      margin: 10px 0;
+      gap: 0 5px;
+      line-height: 18px;
+    }
+    .right-fix-box {
+      right: 17px;
+      bottom: 65px;
+      .item {
+        margin-top: 5px;
+      }
+      .back-top-img {
+        width: 50px;
+        height: 50px;
+      }
+      .collect-icon {
+        width: 50px;
+        height: 50px;
+      }
+    }
+  }
+  .no-auth-wrap {
+    .opcity-box {
+      height: 65px;
+    }
+    .content-box {
+      padding-bottom: 100px;
+    }
+    .icon {
+      width: 100px;
+      height: 100px;
+    }
+  }
+}
+
+.report-detail_hidden{
+  height: 100vh;
+  overflow: hidden;
+}
+</style>

+ 15 - 1
vite.config.js

@@ -8,6 +8,7 @@ import AutoImport from 'unplugin-auto-import/vite';
 import Components from 'unplugin-vue-components/vite';
 import { TDesignResolver } from 'unplugin-vue-components/resolvers';
 import zipBuildPlugin from './zipBuildPlugin';
+import compress from 'vite-plugin-compression'
 
 // https://vitejs.dev/config/
 export default defineConfig(({ mode }) => {
@@ -47,7 +48,20 @@ export default defineConfig(({ mode }) => {
          */
         // customDomId: '__svg__icons__dom__'
       }),
-      zipBuildPlugin()
+      compress({
+        algorithm: 'gzip',
+        ext: '.gz'
+      }),
+      {
+        name: 'custom-zip-plugin',
+        apply: 'build',
+        closeBundle() {
+          setTimeout(() => {
+            const plugin = zipBuildPlugin(ENV.VITE_APP_OUTDIR);
+            return plugin.closeBundle.handler();
+          }, 1000);
+        }
+      }
     ],
     css: {
       // css预处理器

+ 2 - 8
zipBuildPlugin.js

@@ -3,15 +3,9 @@ import { resolve } from 'path';
 import { createWriteStream } from 'fs';
 import archiver from 'archiver';
 
-export default function zipBuildPlugin() {
-  let outDir = 'dist'; // 默认值
-
+export default function zipPlugin(outDir) {
   return {
     name: 'zip-after-build',
-    configResolved(resolvedConfig) {
-      // 读取 Vite 配置中的 build.outDir
-      outDir = resolvedConfig.build.outDir;
-    },
     closeBundle: {
       sequential: true,
       async handler() {
@@ -44,7 +38,7 @@ export default function zipBuildPlugin() {
           archive.pipe(output);
 
           // 将 dist 文件夹及其内容都压缩进 zip
-          archive.directory(outputDir, outDir);
+          archive.directory(outputDir, false);
 
           // 完成归档
           archive.finalize();