ソースを参照

ETA_1.8.1 报告转pdf和长图

hbchen 10 ヶ月 前
コミット
47edbcd68d

+ 2 - 0
.env.lib

@@ -0,0 +1,2 @@
+# 这个webcomponent的库用到 eta_report项目的
+VITE_APP_BASEAPIURL="/api"

+ 1 - 0
.gitignore

@@ -10,6 +10,7 @@ lerna-debug.log*
 node_modules
 dist
 dist-ssr
+lib_dist
 *.local
 package-lock.json
 hongze_ETA_mobile

+ 1 - 0
package.json

@@ -7,6 +7,7 @@
     "dev": "vite",
     "build": "vite build --mode production",
     "build.test": "vite build --mode test",
+    "build.lib": "vite build --mode lib",
     "preview": "vite preview"
   },
   "dependencies": {

+ 199 - 0
src/CustomElement/EtaChart.ce.vue

@@ -0,0 +1,199 @@
+<script setup>
+import {nextTick,ref} from 'vue'
+import {useChartRender} from '@/hooks/chart/render'
+import {chartInfoByCode} from './api/getData.js'
+import {parseQueryString} from "./utils/index"
+const {chartRender}=useChartRender()
+
+const props=defineProps({
+    src:'',
+    width:'',
+    height:'',
+    style:''
+})
+
+// const params=reactive(parseQueryString(props.src))
+const params=parseQueryString(props.src)
+const chartContentEl=ref(null)
+const chartBoxId=ref('')
+const chartInfo=ref(null)
+const dataList=ref(null)
+const haveData=ref(false)
+const markDom=ref(null)
+async function getChartData(){
+    if(!params.code) {
+        haveData.value = false; 
+        return
+    } 
+    try {
+        const res=await chartInfoByCode({
+            UniqueCode:params.code
+        })
+        if(res.Ret!==200) return
+        chartInfo.value=res.Data.ChartInfo
+        dataList.value=res.Data.ChartInfo.Source === 1 ? res.Data.EdbInfoList : [res.Data.EdbInfoList[0]];
+        chartBoxId.value=`hz-chart_${params.code}_${new Date().getTime()}`
+        //处理英文研报英文设置不全就展示中文
+        setLangFromEnReport();
+        haveData.value = true;
+        nextTick(()=>{
+            chartRender({
+                data:{
+                    ...res.Data,
+                    ChartInfo:{
+                        ...res.Data.ChartInfo,
+                        Calendar:'公历'
+                    },
+                },
+                renderId:chartContentEl.value,
+                lang:'zh',
+                changeLangIsCheck:false,
+                showChartTitle:false
+            })
+            res.Data.WaterMark&&(markDom.value.style.backgroundImage = `url(${res.Data.WaterMark})`)
+        })
+    }catch (e) {
+        haveData.value = false; 
+    }
+}
+
+const setLangFromEnReport=()=>{
+    //来源于英文研报
+    if(params.fromPage !== 'en') return
+    let is_name_en = chartInfo.value.ChartNameEn ? true : false;//名称是否有英文
+    let is_target_en = [2,9,10].includes(chartInfo.value.ChartType) ? true : dataList.value.every(_ => _.EdbNameEn);//指标是否有英文
+    params.language = (is_name_en && is_target_en) ? 'en' : 'ch';
+    params.language='ch'
+}
+
+getChartData()
+</script>
+
+<template>
+    <div class="hz-chart-wrap" :style="props.style+';height:'+props.height+'px'" style="margin: 0 0 10px 0;">
+        <header class="hz-chart-head">
+            <span 
+                v-if="chartInfo"
+                class="chart-title" 
+                :style="chartInfo.ChartThemeStyle?`
+                text-align:${JSON.parse(chartInfo.ChartThemeStyle).titleOptions.align};
+                font-size:${JSON.parse(chartInfo.ChartThemeStyle).titleOptions.style.fontSize}px;
+                color:${JSON.parse(chartInfo.ChartThemeStyle).titleOptions.style.color}
+                `:''"
+            >
+                {{ params.language === 'en'?chartInfo.ChartNameEn: chartInfo.ChartName}}
+            </span>
+        </header>
+        <template v-if="haveData">
+            <div :id="chartBoxId" ref="chartContentEl" class="hz-chart-content"></div>
+            <div class="mark" ref="markDom"></div>
+        </template>
+        <div class="notfound" v-else>
+            哎吆,你的图飞了,赶快去找管理员救命吧~
+        </div>
+        <div 
+        class="chart-bottom-info bootom-source"
+        v-if="chartInfo?.Instructions&&JSON.parse(chartInfo?.Instructions).isShow"
+        >
+            <div 
+                class="chart-instruction text_oneLine" 
+                v-text="JSON.parse(chartInfo.Instructions).text"
+                :style="`
+                color: ${JSON.parse(chartInfo.Instructions).color};
+                font-size: ${ JSON.parse(chartInfo.Instructions).fontSize }px
+                `"
+            ></div>
+        </div>
+        <div class="hz-chart-footer" 
+        v-if="chartInfo?.SourcesFrom&&JSON.parse(chartInfo.SourcesFrom)?.isShow"
+        :style="`
+            color: ${ JSON.parse(chartInfo.SourcesFrom).color };
+            font-size: ${ JSON.parse(chartInfo.SourcesFrom).fontSize }px;
+        `"
+        >
+            source:<em>{{ JSON.parse(chartInfo.SourcesFrom).text}}</em>
+        </div>
+    </div>
+</template>
+
+<style lang="scss" scoped>
+div{
+    box-sizing: border-box;
+}
+.hz-chart-wrap{
+    font:initial;
+    page-break-inside: avoid;
+    max-width: 1200PX;
+    height: 100%;
+    overflow: hidden;
+    position: relative;
+    margin: 0 auto;
+    border: 1PX solid rgba(0,0,0,.125) !important;
+    background: #fff;
+    border-radius: 5PX;
+    display: flex;
+    flex-direction: column;
+    .hz-chart-head{
+        box-sizing: border-box;
+        background-color: #F2F2F2;
+        border-bottom: 1px solid rgba(0, 0, 0, 0.125);
+        cursor: pointer;
+        min-height: 40PX;
+        display: flex;
+        align-items: center;
+        padding: 2PX 10PX;
+        .chart-title {
+            width: 100%;
+        }
+    }
+    .hz-chart-content{
+        width: 100%;
+        flex: 1;
+    }
+    .mark{
+        position:absolute;
+        left: 50%;
+        top: 50%;
+        transform: translate(-50%,-50%);
+        background-image: none;
+        background-position:center center;
+        background-repeat:  no-repeat;
+        background-size: contain;
+        pointer-events: none;
+        width: 580PX;
+        height: 60PX;
+    }
+    .notfound {
+        flex: 1;
+        overflow: hidden;
+        padding: 10PX;
+        color: #777;
+        text-align: center;
+    }
+    .chart-bottom-info{
+        padding-right: 10PX;
+        padding-bottom: 0;
+        padding-left: 25PX;
+        display: flex;
+        .chart-instruction {
+            max-width: 80%;
+            flex: 1;
+            text-align: right;
+            margin-left: auto;
+        }
+    }
+    .hz-chart-footer{
+        padding-left: 25PX;
+        padding-right: 10PX;
+        padding-bottom: 10PX;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+    }
+    .text_oneLine{
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+    }
+}
+</style>

+ 147 - 0
src/CustomElement/EtaTable.ce.vue

@@ -0,0 +1,147 @@
+<script setup>
+import {ref} from 'vue'
+import {infoByCode} from './api/getData.js'
+import {parseQueryString} from "./utils/index"
+
+const props=defineProps({
+    src:'',
+    width:'',
+    height:'',
+    style:''
+})
+
+const params=parseQueryString(props.src)
+const info=ref({})
+const showData = ref(false);
+async function getTableData(){
+    const res = await infoByCode({  UniqueCode: params.code, FromScene: Number(params.fromScene||'') });
+    if(res.Ret !== 200) return
+
+    info.value=res.Data
+    showData.value = true;
+}
+
+getTableData()
+</script>
+
+<template>
+    <div 
+        v-if="showData"
+        class="sheet-show-wrapper"  
+    >
+        <h3 class="title">{{info.ExcelName}}</h3>
+        
+        <div class="table-wrapper">
+            <table 
+                cellpadding="0" 
+                cellspacing="0" 
+                :style="`font-size: ${info.Config?.FontSize||12}PX`"
+            >
+                <tbody>
+                <tr 
+                    v-for="(item,index) in info.TableInfo?.TableDataList"
+                    :key="index"
+                >
+                    <td 
+                    :class="['data-cell',{
+                        'one-bg':(index+1)%2&&index>0,
+                        'tow-bg': (index+1)%2!==0&&index>0,
+                        'head-column': index === 0
+                    }]"
+                    v-for="(cell,cell_index) in item"
+                    :key="cell_index"
+                    :colspan="cell.mc.cs||1"
+                    :rowspan="cell.mc.rs||1"
+                    :style="`
+                        color: ${cell.fc};
+                        font-weight: ${cell.bl ? 'bold' : 'normal'};
+                        font-style: ${cell.it ? 'italic' : 'normal'};
+                        background: ${cell.bg};
+                    `"
+                    >
+                    <div class="split-word" v-if="cell.ct.s">
+                        <span 
+                        v-for="(word,word_index) in cell.ct.s" 
+                        :key="`${index}_${cell_index}_${word_index}`"
+                        :style="`
+                            color: ${word.fc};
+                            font-weight: ${word.bl ? 'bold' : 'normal'};
+                            font-style: ${word.it ? 'italic' : 'normal'};
+                        `"
+                        >{{word.v}}</span>
+                    </div>
+                    <div v-else>{{cell.m}}</div>
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+        </div>
+    </div>
+</template>
+
+<style lang="scss" scoped>
+.sheet-show-wrapper {
+  max-width: 1200PX;
+  overflow: hidden;
+  position: relative;
+  margin: 0 auto;
+  background: #fff;
+  .title {
+    font-size: 17PX;
+    font-weight: normal;
+    padding: 0 10PX;
+    margin: 0 0 8PX 0;
+  }
+.table-wrapper {
+    max-width: calc(100vw - 20PX);
+    margin: 0 auto;
+    overflow: auto;
+    table {
+        width: 100%;
+        font-size: 14PX;
+        color: #333;
+        table-layout: auto;
+        td,
+        th {
+            width: auto;
+            height: auto;
+            padding: 0.4em 0;
+            word-break: break-all;
+            word-wrap: break-word;
+            line-height: 1.2em;
+            border: 1PX solid #dcdfe6;
+            text-align: center;
+            background-color: #fff;
+            border-left: none;
+            border-top: none;
+            &:first-child {
+                border-left: 1PX solid #dcdfe6;
+            }
+        }
+
+        .data-cell{
+            color: #333;
+            &.one-bg {
+            background-color: #EFEEF1;
+            }
+            &.two-bg {
+            background-color: #fff;
+            }
+        }
+
+        .thead-sticky {
+            position: sticky;
+            top: 0;
+        }
+
+        .head-column {
+            background-color: #505B78;
+            color: #fff;
+        }
+        .split-word {
+            span { display: inline; }
+        }
+    }
+}
+}
+</style>

+ 20 - 0
src/CustomElement/api/getData.js

@@ -0,0 +1,20 @@
+//获取数据
+import { get,post } from "./index";
+
+
+// 接口
+/**
+ * 通过code获取图表详情
+ * @param UniqueCode 
+*/
+export const chartInfoByCode=params=>{
+  return post('/chart/detail',params)
+}
+
+/**
+ * 通过code获取表格详情
+ * @param UniqueCode 
+*/
+export const infoByCode=params=>{
+    return post('/excel/detail',params)
+}

+ 47 - 0
src/CustomElement/api/index.js

@@ -0,0 +1,47 @@
+"use strict";
+import axios from "axios";
+
+// import.meta.env.VITE_APP_BASEAPIURL
+let config = {
+  baseURL: import.meta.env.VITE_APP_BASEAPIURL,
+  timeout: 10*60 * 1000,
+};
+const _axios = axios.create(config);
+
+_axios.interceptors.request.use(
+  function (config) {
+    return config;
+  },
+  function (error) {
+    console.error(error);
+    return Promise.reject(error);
+  }
+);
+_axios.interceptors.response.use(
+  function (response) {
+    let data=response.data  
+    if(response.status!==200){
+      console.error('网络异常');
+    }
+    if(!data){
+      console.error('服务器开了个小差');
+    }
+    if(data.Ret===408){//token失效
+      console.error(data.Msg);
+    }
+    if(data.Ret===403){
+      console.error(data.Msg||'网络异常');
+    }
+    return data;
+  },
+  function (error) {
+    console.error(error);
+    return Promise.reject(error);
+  }
+);
+export const get = (url, params) => {
+  return _axios.get(url, { params });
+};
+export const post = (url, params) => {
+  return _axios.post(url, params);
+};

+ 11 - 0
src/CustomElement/index.js

@@ -0,0 +1,11 @@
+import { defineCustomElement } from 'vue';
+import EtaChartComp from './EtaChart.ce.vue';
+import EtaTableComp from './EtaTable.ce.vue'
+
+const EtaChart = defineCustomElement(EtaChartComp);
+const EtaTable = defineCustomElement(EtaTableComp);
+
+export function registerEtaComp() {
+  customElements.define('eta-chart', EtaChart)
+  customElements.define('eta-table', EtaTable)
+}

+ 20 - 0
src/CustomElement/utils/index.js

@@ -0,0 +1,20 @@
+// 获取url字符串中所有参数
+export function parseQueryString(url) {
+  // 如果链接地址不包含 "?",则没有查询参数,直接返回空对象
+  if (url.indexOf('?') === -1) {
+    return {};
+  }
+  // 获取 "?" 后面的查询参数部分
+  const queryString = url.split('?')[1];
+  // 将查询参数字符串拆分为键值对数组
+  const paramsArray = queryString.split('&');
+  // 创建一个对象来存储解析后的参数
+  const paramsObject = {};
+  // 遍历键值对数组,将每一对解析并存储到对象中
+  paramsArray.forEach(param => {
+    const [key, value] = param.split('=');
+    // 对于类似 "%20" 这样的编码,使用 decodeURIComponent 进行解码
+    paramsObject[key] = decodeURIComponent(value);
+  });
+  return paramsObject;
+}

+ 9 - 0
src/api/report.js

@@ -231,4 +231,13 @@ export default {
     reportCnCancel(params){
         return post("/report/approve/cancel",params)
     },
+    /**
+     * 研报转pdf、长图
+     * @param {Object} params 
+     * @param {Number} params.ReportId
+     * @returns 
+     */
+    report2PdfImg(params){
+        return post("/smart_report/get_pdf_url",params)
+    },
 }

+ 1 - 1
src/hooks/chart/render.js

@@ -2288,7 +2288,7 @@ function setRadarChart({DataResp,EdbInfoList,ChartInfo}) {
     return {
       chart: {
         ...chartDefaultOpts.chart,
-        ...chartTheme.drawOption,
+        ...chartTheme?.drawOption,
         spacing: [2,10,2,10],
         polar:true,
       },

+ 4 - 0
src/hooks/useAuthBtn.js

@@ -10,6 +10,8 @@ export const reportManageBtn = {
     reportManage_reportDel:'reportManage:reportDel',//删除研报
     reportManage_reportEdit:'reportManage:reportEdit',//编辑研报
     reportManage_cancelPublish:'reportManage:cancelPublish',//取消发布
+    reportManage_exportPdf:'reportManage:exportPdf',//下载pdf
+    reportManage_exportImg:'reportManage:exportImg',//下载长图
     reportManage_publish:'reportManage:publish',//发布研报
     reportManage_reportList:'reportManage:reportList',//研报列表的选项
     reportManage_reportList_uv:'reportManage:reportList:uv',//研报列表-PV/UV
@@ -23,6 +25,8 @@ export const enReportManageBtn = {
     enReport_reportDel:'enReport:reportDel',//删除研报
     enReport_reportEdit:'enReport:reportEdit',//编辑研报
     enReport_cancelPublish:'enReport:cancelPublish',//取消发布
+    enReport_exportPdf:'enReport:exportPdf',//下载pdf
+    enReport_exportImg:'enReport:exportImg',//下载长图
     enReport_publish:'enReport:publish',//发布研报
     enReport_reportAdd:'enReport:reportAdd',//添加研报
 }

+ 28 - 1
src/views/report/List.vue

@@ -9,9 +9,15 @@ import { useRouter } from 'vue-router';
 import { useWindowSize } from '@vueuse/core'
 import {useCachedViewsStore} from '@/store/modules/cachedViews'
 import {reportFrequencyOpts} from './utils/config'
+import {useDownLoadFile} from '@/hooks/useDownLoadFile'
 import {reportManageBtn,useAuthBtn} from '@/hooks/useAuthBtn'
 import {useReportApprove} from '@/hooks/useReportApprove'
+import {usePublicSettingStore} from '@/store/modules/publicSetting'
+
 const cachedViewsStore=useCachedViewsStore()
+const publicSettingStore = usePublicSettingStore()
+
+const {startDownload}=useDownLoadFile()
 const {isApprove,isOtherApprove,getEtaConfig} = useReportApprove()
 
 const {checkAuthBtn} = useAuthBtn()
@@ -216,7 +222,24 @@ function handleReportCancel(item){
         })
     }).catch(()=>{})
 }
-
+// type 1-pdf 2-长图
+function downloadPdfImg(item,type){
+    console.log(item,type,'item,type');
+    showReportItemOpt.value=false
+    showToast('开始下载')
+    // 设置水印文案
+    // let reportUrl = `${baseUrl}/reportshare_pdf?code=${item.ReportCode}&flag=${waterMarkStr}&viewType=!pdf!`
+    let reportUrl = `${publicSettingStore.publicSetting.ReportViewUrl}/reportshare_pdf?code=${item.ReportCode}&viewType=!pdf!`
+    apiReport.report2PdfImg({ReportUrl:reportUrl,ReportCode:item.ReportCode,Type:type}).then(res=>{
+        if(res.Ret == 200 && res.Data){
+            if(res.Data.endsWith('.pdf')){
+                window.open(res.Data,"_blank")
+            }else{
+                startDownload(res.Data,`${item.Title}.${res.Data.split('.')[res.Data.split('.').length-1]}`)
+            }
+        }
+    })
+}
 
 // 发布弹窗关闭
 function handlePublishPopClose(refresh){
@@ -697,6 +720,10 @@ onMounted(async ()=>{
                     @click="handleReportPublishCancle(activeItem)">撤销</div> <!-- 实际上是取消发布 -->
                 <div class="item" v-if="checkAuthBtn(reportManageBtn.reportManage_cancelPublish)&&activeItem.State===6"
                     @click="handleReportCancel(activeItem)">撤销</div>
+                <div class="item" v-if="checkAuthBtn(reportManageBtn.reportManage_exportPdf)"
+                    @click="downloadPdfImg(activeItem,1)">下载pdf</div>
+                <div class="item" v-if="checkAuthBtn(reportManageBtn.reportManage_exportImg)"
+                    @click="downloadPdfImg(activeItem,2)">下载长图</div>   
                 <div class="item" @click="handldReportMsgSend(activeItem)" v-if="activeItem.MsgIsSend==0&&checkAuthBtn(reportManageBtn.reportManage_sendMsg)">推送消息</div>
             </template>
             <!-- 待审批,已驳回 -->

+ 30 - 0
src/views/reportEn/List.vue

@@ -1,6 +1,7 @@
 <script setup name="ReportEnList">
 import {nextTick, reactive,ref,computed,onMounted} from 'vue'
 import apiReportEn from '@/api/reportEn'
+import apiReport from '@/api/report'
 import moment from 'moment'
 import ListClassify from './components/ListClassify.vue'
 import SendEmail from './components/SendEmail.vue';
@@ -9,9 +10,15 @@ import { showToast,showDialog,Dialog } from 'vant';
 import { useRouter } from 'vue-router';
 import { useWindowSize } from '@vueuse/core'
 import {useCachedViewsStore} from '@/store/modules/cachedViews'
+import {usePublicSettingStore} from '@/store/modules/publicSetting'
+import {useDownLoadFile} from '@/hooks/useDownLoadFile'
 import {enReportManageBtn,useAuthBtn} from '@/hooks/useAuthBtn'
 import {useReportApprove} from '@/hooks/useReportApprove'
+
 const cachedViewsStore=useCachedViewsStore()
+const publicSettingStore = usePublicSettingStore()
+
+const {startDownload}=useDownLoadFile()
 const {checkAuthBtn} = useAuthBtn()
 const {isApprove,isOtherApprove,getEtaConfig} = useReportApprove()
 
@@ -162,6 +169,25 @@ function handleReportPublishCancle(item){
     })
 }
 
+// type 1-pdf 2-长图
+function downloadPdfImg(item,type){
+    console.log(item,type,'item,type');
+    showReportItemOpt.value=false
+    showToast('开始下载')
+    // 设置水印文案
+    // let reportUrl = `${baseUrl}/reportshare_pdf?code=${item.ReportCode}&flag=${waterMarkStr}&viewType=!pdf!`
+    let reportUrl = `${publicSettingStore.publicSetting.ReportViewUrl}/reportshare_pdf?code=${item.ReportCode}&viewType=!pdf!`
+    apiReport.report2PdfImg({ReportUrl:reportUrl,ReportCode:item.ReportCode,Type:type}).then(res=>{
+        if(res.Ret == 200 && res.Data){
+            if(res.Data.endsWith('.pdf')){
+                window.open(res.Data,"_blank")
+            }else{
+                startDownload(res.Data,`${item.Title}.${res.Data.split('.')[res.Data.split('.').length-1]}`)
+            }
+        }
+    })
+}
+
 //提交报告
 function handleReportSubmit(item){
     showDialog({
@@ -660,6 +686,10 @@ onMounted(()=>{
                     @click="handleReportPublishCancle(activeItem)">撤销</div> <!-- 实际上是取消发布 -->
                 <div class="item" v-if="checkAuthBtn(enReportManageBtn.enReport_cancelPublish)&&activeItem.State===6"
                     @click="handleReportCancle(activeItem)">撤销</div>
+                <div class="item" v-if="checkAuthBtn(enReportManageBtn.enReport_exportPdf)"
+                    @click="downloadPdfImg(activeItem,1)">下载pdf</div>
+                <div class="item" v-if="checkAuthBtn(enReportManageBtn.enReport_exportImg)"
+                    @click="downloadPdfImg(activeItem,2)">下载长图</div>   
             </template>
             <!-- 待审批,已驳回 -->
             <template v-if="[4,5].includes(activeItem.State)">

+ 33 - 4
vite.config.js

@@ -10,11 +10,39 @@ import { NodeModulesPolyfillPlugin } from '@esbuild-plugins/node-modules-polyfil
 import rollupNodePolyFill from 'rollup-plugin-node-polyfills'
 
 // https://vitejs.dev/config/
-export default ({ mode }) =>
-  defineConfig({
+export default ({ mode }) =>{
+  // eta webComponent组件打包配置
+  const etaWebCompBuildConfig={
+    outDir:'lib_dist',
+    copyPublicDir:false,
+    // minify:false,
+    lib: {
+      entry: path.resolve(__dirname,'./src/CustomElement/index.js'),
+      formats: ["es"],
+      name: "eta",
+      fileName:'eta_comp'
+    },
+    rollupOptions:{
+      external: ['vue'],
+      output: {
+        // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
+        globals: {
+          vue: 'Vue',
+        },
+      },
+    }
+  }
+
+  return defineConfig({
     base: loadEnv(mode, process.cwd()).VITE_APP_BASE_URL, // 若服务器不是将该项目放在根目录的则 需要此设置 和服务器上同名
     plugins: [
-      vue(),
+      vue({
+        template:{
+          compilerOptions:{
+            isCustomElement:tag=>tag.startsWith('eta-')
+          }
+        }
+      }),
       Components({
         resolvers: [VantResolver()],
       }),
@@ -64,7 +92,7 @@ export default ({ mode }) =>
           ]
         }
       },
-    build: {
+    build: mode=='lib'?etaWebCompBuildConfig:{
       outDir: loadEnv(mode, process.cwd()).VITE_APP_OUTDIR,
       rollupOptions: {
         plugins: [
@@ -77,3 +105,4 @@ export default ({ mode }) =>
       host:true
     }
   });
+}