Browse Source

发布ppt

jwyu 2 years ago
parent
commit
1737d6f8c4

+ 3 - 0
index.html

@@ -9,5 +9,8 @@
   <body>
     <div id="app"></div>
     <script type="module" src="/src/main.js"></script>
+    <script src="/jquery-3.6.0.min.js"></script>
+    <!-- oss SDK -->
+	  <script type="text/javascript" src="https://gosspublic.alicdn.com/aliyun-oss-sdk-6.16.0.min.js"></script>
   </body>
 </html>

+ 2 - 0
package.json

@@ -13,11 +13,13 @@
     "@vueuse/core": "^9.13.0",
     "axios": "^1.3.4",
     "highcharts": "^10.3.3",
+    "himalaya": "^1.1.0",
     "js-base64": "^3.7.5",
     "js-md5": "^0.7.3",
     "lodash": "^4.17.21",
     "moment": "^2.29.4",
     "normalize.css": "^8.0.1",
+    "pptxgenjs": "^3.12.0",
     "vant": "^4.1.2",
     "vue": "^3.2.47",
     "vue-router": "^4.1.6"

File diff suppressed because it is too large
+ 1 - 0
public/jquery-3.6.0.min.js


BIN
public/pptImg/ppt_last_img.png


BIN
public/pptImg/pptcover_bg3.jpg


BIN
public/pptImg/pptitem_bg.png


+ 9 - 0
src/api/common.js

@@ -0,0 +1,9 @@
+/**
+ * 公共接口模块
+ */
+
+import {get,post} from './index'
+
+export function apiGetOSSSign(){
+    return get('/resource/oss/get_sts_token',{})
+}

+ 2 - 1
src/api/index.js

@@ -49,7 +49,8 @@ _axios.interceptors.response.use(
     //关闭loading
     LOADINGCOUNT--;
     if (LOADINGCOUNT === 0) {
-      closeToast()
+      // closeToast()
+      LOADING.close()
     }
 
     if(response.status!==200){

+ 9 - 0
src/api/ppt.js

@@ -68,4 +68,13 @@ export function apiPPTCopy(params){
  */
 export function apiPPTDetail(params){
     return get('/pptv2/detail',params)
+}
+
+/**
+ * 发布ppt
+ * @param PptId
+ * @param PptxUrl
+ */
+export function apiPPTPublish(params){
+    return post('/pptv2/publish',params)
 }

BIN
src/assets/imgs/ppt/icon_action_copy.png


BIN
src/assets/imgs/ppt/icon_action_more.png


BIN
src/assets/imgs/ppt/icon_action_play.png


BIN
src/assets/imgs/ppt/icon_action_publish.png


+ 62 - 0
src/hooks/useUploadFileToOSS.js

@@ -0,0 +1,62 @@
+// 上传文件到阿里云oss
+import {apiGetOSSSign} from '@/api/common'
+
+/**
+ * 上传到oss
+ * @param data 文件数据
+ * @param fileName 存放在oss中的文件路径包含文件名
+ * @param isMultipart 是否分片上传
+ * @returns fileUrl 返回文件在阿里云上的地址
+ */
+export async function useUploadFileToOSS(data,fileName,isMultipart=false){
+    const signRes=await apiGetOSSSign()
+    if(signRes.Ret!==200) return
+    const accessKeyId=signRes.Data.AccessKeyId
+    const accessKeySecret=signRes.Data.AccessKeySecret
+    const stsToken=signRes.Data.SecurityToken
+
+    const ALOSSINS=new OSS({
+        // yourRegion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。
+        region: "oss-cn-shanghai",
+        // 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
+        accessKeyId: accessKeyId,
+        accessKeySecret: accessKeySecret,
+        // 从STS服务获取的安全令牌(SecurityToken)。
+        stsToken: stsToken,
+        // 填写Bucket名称,例如examplebucket。
+        bucket: "hzchart",
+        endpoint:'hzstatic.hzinsights.com',
+        cname:true,
+        timeout:6000000
+    });
+
+    let res=null,resUrl='';
+    try {
+        if(isMultipart){
+            const options = {
+                // 获取分片上传进度、断点和返回值。
+                progress: (p, cpt, res) => {
+                    // console.log(p);
+                    // ALOSSAbortCheckpoint=cpt
+                    // this.percentage=parseInt(p*100)
+                },
+                // 设置并发上传的分片数量。
+                parallel: 10,
+                // 设置分片大小。默认值为1 MB,最小值为100 KB。
+                partSize: 1024 * 1024 * 10, // 10MB
+            };
+            res=await ALOSSINS.multipartUpload(fileName,data,{...options})
+        }else{
+            res=await ALOSSINS.put(fileName,data)
+        }
+
+        if(res.res.status===200){
+            resUrl='https://hzstatic.hzinsights.com/'+res.name
+        }
+    } catch (error) {
+        console.log(error);
+    }
+    
+
+    return resUrl
+}

+ 71 - 13
src/views/ppt/Detail.vue

@@ -1,9 +1,10 @@
 <script setup>
-import {computed, ref,nextTick} from 'vue'
+import { ref,nextTick} from 'vue'
 import { useRoute } from "vue-router";
 import { vElementSize } from '@vueuse/components'
 import {apiPPTDetail} from '@/api/ppt'
 import {createPPTContent,getTemplate} from './hooks/createPPTContent'
+import {usePPTPublish} from './hooks/usePPTPublish'
 
 const route=useRoute()
 
@@ -17,37 +18,94 @@ async function getPPTDetail(){
     const res=await apiPPTDetail({PptId:Number(pptId)})
     conArr.value=createPPTContent(res.Data)
     nextTick(()=>{
-        onResize({width:document.getElementsByClassName('ppt-detail-page')[0].clientWidth})
+        onResize({width:document.getElementsByClassName('ppt-content-wrap')[0].clientWidth})
     })
 }
 getPPTDetail()
 
 // 监听页面尺寸变化缩放页面
-function onResize({width}){
+let pptContentHeight=ref(0)//ppt内容缩放后的真实高度
+function onResize({width,height}){
     const scale=width/900
-    const el=document.getElementsByClassName('ppt-detail-page')[0]
+    const el=document.getElementsByClassName('ppt-content-wrap')[0]
     if(!el) return
+    pptContentHeight.value=height*scale
     el.style.transform=`scale(${scale})`
 }
 
+// 点击发布PPT
+function handllePublishPPT(){
+    usePPTPublish(conArr.value,pptId)
+}
+
 
 </script>
 
 <template>
-    <div class="ppt-detail-page" v-element-size="onResize">
-        <template v-for="(item,index) in conArr" :key="item.id">
-            <component 
-                :is="getTemplate(item.modelId)" 
-                :pageData="{...item,pptPageIndex:index}"
-            />
-        </template>
+    <div class="ppt-detail-page" :style="{height:pptContentHeight+'px'}">
+        <div class="ppt-content-wrap" v-element-size="onResize">
+            <template v-for="(item,index) in conArr" :key="item.id">
+                <component 
+                    :is="getTemplate(item.modelId)" 
+                    :pageData="{...item,pptPageIndex:index}"
+                />
+            </template>
+        </div>
+        <div class="mobile-fix-bot-warp">
+            <div class="item-box" @click="handllePublishPPT">
+                <img src="@/assets/imgs/ppt/icon_action_publish.png" alt="">
+                <span>发布</span>
+            </div>
+            <div class="item-box">
+                <img src="@/assets/imgs/ppt/icon_action_play.png" alt="">
+                <span>播放</span>
+            </div>
+            <div class="item-box">
+                <img src="@/assets/imgs/ppt/icon_action_copy.png" alt="">
+                <span>复制</span>
+            </div>
+            <div class="item-box">
+                <img src="@/assets/imgs/ppt/icon_action_more.png" alt="">
+                <span>更多</span>
+            </div>
+        </div>
     </div>
 </template>
 
 <style lang="scss" scoped>
 .ppt-detail-page{
     width: 100%;
-    position: absolute;
-    transform-origin: 0 0;
+    .ppt-content-wrap{
+        width: 100%;
+        position: absolute;
+        transform-origin: 0 0;
+    }
+
+}
+.mobile-fix-bot-warp{
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    height: 100px;
+    background: #FFFFFF;
+    border-top: 1px solid $border-color;
+    z-index: 999;
+    display: flex;
+    .item-box{
+        height: 100%;
+        flex: 1;
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+        img{
+            width: 44px;
+        }
+        span{
+            font-size: 20px;
+        }
+    }
+
 }
 </style>

+ 1 - 1
src/views/ppt/components/ChartWrap.vue

@@ -19,7 +19,7 @@ const renderId=computed(()=>{
 // 获取图表详情
 let chartIsDelete=ref(false)//图表是否被删除
 async function getChartInfo(){
-    const res=await apiChartInfoByCode({UniqueCode:props.itemData.chartId})
+    const res=await apiChartInfoByCode({UniqueCode:props.itemData.chartId,IsCache: true})
     if(res.Ret===200){
         if(!res.Data.ChartInfo){
             chartIsDelete.value=true

+ 1 - 1
src/views/ppt/components/ImageWrap.vue

@@ -9,7 +9,7 @@ const props=defineProps({
 
 <template>
     <div class="image-box">
-        <img :src="itemData.src" alt="">
+        <img :id="'image_'+itemData.pptPageIndex+'_'+itemData.position" :src="itemData.src" alt="">
     </div>
 </template>
 

+ 2 - 17
src/views/ppt/components/layers/LineShape.vue

@@ -1,6 +1,5 @@
 <script setup>
-import {computed,onMounted,ref} from 'vue'
-import { useDebounceFn  } from '@vueuse/core'
+import {computed} from 'vue'
 import LineMarker from './LineMarker.vue'
 const props=defineProps({
     itemData:{
@@ -19,19 +18,6 @@ const svgHeight=computed(()=>{
     return height < 25 ? 25 : height
 }) 
 
-// 需要单独缩放图层模块
-let scale=ref('')
-const domResize=useDebounceFn (()=>{
-    console.log('更新layer大小');
-    const w= document.getElementsByClassName('ppt-detail-page')[0].clientWidth
-    scale.value=`scale(${w/906},${w/906})`
-},0)
-
-onMounted(() => {
-    window.addEventListener("resize", () => {
-        // domResize()
-    }, false)
-})
 
 </script>
 
@@ -44,8 +30,7 @@ onMounted(() => {
         class="line-shape-box"
         :style="{
             top: itemData.percentageTop*100 + '%',
-            left: itemData.percentageLeft*100 + '%',
-            transform:scale
+            left: itemData.percentageLeft*100 + '%'
         }"
     >
         <svg 

+ 0 - 18
src/views/ppt/components/layers/RectShape.vue

@@ -1,6 +1,4 @@
 <script setup>
-import {onMounted,ref} from  'vue'
-import { useDebounceFn  } from '@vueuse/core'
 const props=defineProps({
     itemData:{
         type:Object,
@@ -8,21 +6,6 @@ const props=defineProps({
     }
 })
 
-// 需要单独缩放图层模块
-let scale=ref('')
-const domResize=useDebounceFn (()=>{
-    console.log('更新layer大小');
-    const w= document.getElementsByClassName('ppt-detail-page')[0].clientWidth
-    console.log('ppt-detail-page',w);
-    scale.value=`scale(${w/906},${w/906})`
-},0)
-
-onMounted(() => {
-    window.addEventListener("resize", () => {
-        // domResize()
-    }, false)
-})
-
 
 </script>
 
@@ -32,7 +15,6 @@ onMounted(() => {
         :style="{
             top: itemData.percentageTop*100 + '%',
             left: itemData.percentageLeft*100 + '%',
-            transform:scale
         }"
     >
         <svg 

+ 0 - 1
src/views/ppt/components/layers/TextShape.vue

@@ -30,7 +30,6 @@ const props=defineProps({
 .text-shape-box{
     position: absolute;
     pointer-events: none;
-    // padding: 10PX;
     .text-content{
         width: 100%;
         height: 100%;

+ 2 - 2
src/views/ppt/hooks/createPPTContent.js

@@ -1,7 +1,7 @@
 
 import {formatPPTDate} from '../utils/index'
 import {bgList} from '../utils/config'
-import {ref,markRaw} from 'vue'
+import {ref} from 'vue'
 import Cover from '../template/Cover.vue'
 import Footer from '../template/Footer.vue'
 import FormatOne from '../template/FormatOne.vue'
@@ -66,7 +66,7 @@ export function createPPTContent(params){
         modelId:-1,
         id:-1,
         //英文地址:https://hzstatic.hzinsights.com/static/ppt_m/ppt_last__img_en.png
-        bgImg:'https://hzstatic.hzinsights.com/static/ppt_m/ppt_last_img.png',
+        bgImg:'/pptImg/ppt_last_img.png',
     }
     arr.push(lastPageData)
     

+ 656 - 0
src/views/ppt/hooks/usePPTPublish.js

@@ -0,0 +1,656 @@
+// ppt发布逻辑
+import {apiSheetInfoByCode} from '@/api/sheet.js'
+import {apiPPTPublish} from '@/api/ppt.js'
+import { showToast,showLoadingToast } from 'vant';
+import pptxgen from "pptxgenjs";
+import { parse } from "himalaya";
+import _ from 'lodash'
+import {pptLayout,pptSlideMaster,pptSlideMasterEn,modelConfig} from '../utils/config'
+import {useUploadFileToOSS} from '@/hooks/useUploadFileToOSS'
+import moment from 'moment'
+
+let LoadingINS=null
+let PPTContentList=[]
+let pptId=0
+
+
+// 校验ppt内容是否完整
+function checkPPT(){
+    for (let index = 1; index < PPTContentList.length-1; index++) {
+        const element = PPTContentList[index];
+        if(!element.title){
+            console.log(`第${index}页内容不完整,请在pc端重新编辑!`);
+            showToast(`第${index}页内容不完整,请在pc端重新编辑!`);
+            return false
+        }
+        if(!element.elements.length===0){
+            showToast(`第${index}页内容不完整,请在pc端重新编辑!`);
+            return false
+        }
+    }
+    return true
+}
+
+/**
+ * PPT默认配置
+ * @param pptx ppt插件实例
+ * @param LayoutType ppt版式 1-10:7,2-16:9,3-4:3
+ * @param lang 
+ */
+function PPTInit(pptx,LayoutType,lang='ch'){
+    let layout = pptLayout
+    let sliderMaster = lang==='ch'?pptSlideMaster:pptSlideMasterEn
+    if(LayoutType!==1){
+        layout = { name: "myppt", width: 10, height: LayoutType===2?5.625:7.5 }
+        const y = lang==='ch'?0:-0.1
+        const h = LayoutType===2?5.625:7.5
+        sliderMaster.objects[1] = {image: {x:0,y:y,w:10,h:lang==='ch'?h:h+0.1,path: lang==='ch'?"/pptImg/pptitem_bg.png":"/pptImg/pptitem_bg.png"}}
+        sliderMaster.slideNumber = {x:'95%',y:LayoutType===2?'92%':'95%',fontSize:12}
+    }
+    pptx.defineLayout(layout)
+    pptx.layout = layout.name
+    pptx.defineSlideMaster(sliderMaster)
+    return pptx
+}
+
+// svg转base64图片,参考:https://github.com/scriptex/svg64
+const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
+const PREFIX = 'data:image/svg+xml;base64,'
+const utf8Encode = (string) => {
+  string = string.replace(/\r\n/g, '\n')
+  let utftext = ''
+  for (let n = 0; n < string.length; n++) {
+    const c = string.charCodeAt(n)
+    if (c < 128) {
+      utftext += String.fromCharCode(c)
+    }
+    else if (c > 127 && c < 2048) {
+      utftext += String.fromCharCode((c >> 6) | 192)
+      utftext += String.fromCharCode((c & 63) | 128)
+    }
+    else {
+      utftext += String.fromCharCode((c >> 12) | 224)
+      utftext += String.fromCharCode(((c >> 6) & 63) | 128)
+      utftext += String.fromCharCode((c & 63) | 128)
+    }
+  }
+  return utftext
+}
+const encode = (input) => {
+  let output = ''
+  let chr1, chr2, chr3, enc1, enc2, enc3, enc4
+  let i = 0
+  input = utf8Encode(input)
+  while (i < input.length) {
+    chr1 = input.charCodeAt(i++)
+    chr2 = input.charCodeAt(i++)
+    chr3 = input.charCodeAt(i++)
+    enc1 = chr1 >> 2
+    enc2 = ((chr1 & 3) << 4) | (chr2 >> 4)
+    enc3 = ((chr2 & 15) << 2) | (chr3 >> 6)
+    enc4 = chr3 & 63
+    if (isNaN(chr2)) enc3 = enc4 = 64
+    else if (isNaN(chr3)) enc4 = 64
+    output = output + characters.charAt(enc1) + characters.charAt(enc2) + characters.charAt(enc3) + characters.charAt(enc4)
+  }
+  return output
+}
+function svg2Base64 (element) {
+    const XMLS = new XMLSerializer()
+    const svg = XMLS.serializeToString(element)
+    return PREFIX + encode(svg)
+    //return PREFIX + new http.Base64().encode(svg)
+}
+
+/**
+ * 获取图表图片
+ * @param id 图表dom中的id
+ */
+function changeUrl(id){
+    console.log(id);
+    let img = new Image()
+    // const svgHtml = $(`#${id} svg`)[0].outerHTML
+    /* img.src = `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgHtml)))}` */
+    img.src = svg2Base64($(`#${id} svg`)[0]);//不支持unescape时可用
+    const imgW = $(`#${id}`)[0].offsetWidth*4,imgH = $(`#${id}`)[0].offsetHeight*4
+    let canvas = document.createElement('canvas')
+    canvas.width = imgW
+    canvas.height = imgH
+    return new Promise(resolve=>{
+        img.onload = ()=>{
+            //console.log('src',img.src)
+            let ctx = canvas.getContext("2d")
+            ctx.drawImage(img, 0, 0, imgW, imgH) 
+            resolve(canvas.toDataURL("image/png"))
+        }
+    })   
+}
+
+//解析富文本,参考https://github.com/pipipi-pikachu/PPTist/tree/master/src/utils/htmlParser
+//将text中被转义的字符转义回来,如:a<b 在富文本内会被转义成 a&lt;b
+function replacer (_, p1){
+    return {
+        "&lt;": "<",
+        "&gt;": ">",
+        "&ldquo;": "“",
+        "&rdquo;": "”",
+        "&amp;":"&",
+        "&lsquo;":"‘",
+        "&rsquo;":"’",
+        "&mdash;":'——',
+        "&ge;":'≥',
+        "&le;":'≤',
+        "&middot;":'·'
+    } [p1]
+}
+function toJson (html) {
+    const json = parse(html)
+    //console.log('json', json)
+    return json
+}
+function toTextProps (json) {
+    const slices = []
+    let bulletFlag = false
+    //剔除掉ul/li里面多余的\n
+    for(const item of json){
+      if(item.tagName==='ul'||item.tagName==='ol'){
+        item.children = item.children.filter(i=>i.type!=='text')
+      }
+    }
+    //console.log('json',json)
+    const _parse = (json, baseStyleObj = {}) => {
+      for (const item of json) {
+        const isBlockTag = 'tagName' in item && ['div', 'li', 'p'].includes(item.tagName)
+        if (isBlockTag && slices.length) {
+          const lastSlice = slices[slices.length - 1]
+          if (!lastSlice.options) lastSlice.options = {}
+          lastSlice.options.breakLine = true
+        }
+        const styleObj = { ...baseStyleObj }
+        const styleAttr = 'attributes' in item ? item.attributes.find(attr => attr.key === 'style') : null
+        if (styleAttr && styleAttr.value) {
+          const styleArr = styleAttr.value.split(';')
+          for (const styleItem of styleArr) {
+            const [_key, _value] = styleItem.split(': ')
+            const [key, value] = [_key ? _key.trim() : '', _value ? _value.trim() : '']
+            if (key && value) styleObj[key] = value
+          }
+        }
+        if ('tagName' in item) {
+          if (item.tagName === 'em') {
+            styleObj['font-style'] = 'italic'
+          }
+          if (item.tagName === 'strong') {
+            styleObj['font-weight'] = 'bold'
+          }
+          if (item.tagName === 'sup') {
+            styleObj['vertical-align'] = 'super'
+          }
+          if (item.tagName === 'sub') {
+            styleObj['vertical-align'] = 'sub'
+          }
+          if (item.tagName === 'a') {
+            const attr = item.attributes.find(attr => attr.key === 'href')
+            styleObj['href'] = attr || ''
+          }
+          if (item.tagName === 'ul') {
+            styleObj['list-type'] = 'ul'
+          }
+          if (item.tagName === 'ol') {
+            styleObj['list-type'] = 'ol'
+          }
+          if (item.tagName === 'li') {
+            bulletFlag = true
+          }
+        }
+        if ('tagName' in item && item.tagName === 'br') {
+          slices.push({ text: '', options: { breakLine: true } })
+        } else if ('content' in item) {
+              const transStr = /(&lt;|&gt;|&ldquo;|&rdquo;|&amp;|&lsquo;|&rsquo;|&mdash;|&ge;|&le;|&middot;)/g
+              const text = item.content.replace(/\n/g, '').replace(/&nbsp;/g, ' ').replace(transStr, replacer)
+              const options = {}
+  
+              if (styleObj['font-size']&&styleObj['font-size']!='initial') {
+                  options.fontSize = parseInt(styleObj['font-size']) * 0.75
+              }
+              if (styleObj['color']&&styleObj['color']!='initial') {
+                  options.color = styleObj['color']
+              }
+              if (styleObj['background-color']&&styleObj['background-color']!='initial') {
+                  options.highlight = styleObj['background-color']
+              }
+              if (styleObj['text-decoration-line']) {
+                  if (styleObj['text-decoration-line'].indexOf('underline') !== -1) {
+                      options.underline = {
+                          color: options.color || '#000000',
+                          style: 'sng',
+                      }
+                  }
+                  if (styleObj['text-decoration-line'].indexOf('line-through') !== -1) {
+                      options.strike = 'sngStrike'
+                  }
+              }
+              if (styleObj['text-decoration']) {
+                  if (styleObj['text-decoration'].indexOf('underline') !== -1) {
+                      options.underline = {
+                          color: options.color || '#000000',
+                          style: 'sng',
+                      }
+                  }
+                  if (styleObj['text-decoration'].indexOf('line-through') !== -1) {
+                      options.strike = 'sngStrike'
+                  }
+              }
+              if (styleObj['vertical-align']) {
+                  if (styleObj['vertical-align'] === 'super') options.superscript = true
+                  if (styleObj['vertical-align'] === 'sub') options.subscript = true
+              }
+              if (styleObj['text-align']&&styleObj['text-align']!='initial') options.align = styleObj['text-align']
+              if (styleObj['font-weight']) options.bold = styleObj['font-weight'] === 'bold'
+              if (styleObj['font-style']) options.italic = styleObj['font-style'] === 'italic'
+              if (styleObj['font-family']&&styleObj['font-family']!='initial') options.fontFace = styleObj['font-family']
+              if (styleObj['href']) options.hyperlink = {
+                  url: styleObj['href']
+              }
+              if (bulletFlag && styleObj['list-type'] === 'ol') {
+                options.bullet = { type: 'number', indent: 20 * 0.75 }
+                options.paraSpaceBefore = 0.1
+                bulletFlag = false
+              }
+              if (bulletFlag && styleObj['list-type'] === 'ul') {
+                options.bullet = { indent: 20 * 0.75 }
+                options.paraSpaceBefore = 0.1
+                bulletFlag = false
+              }
+              slices.push({ text, options })
+          } else if ('children' in item) _parse(item.children, styleObj)
+      }
+    }
+    _parse(json)
+    //console.log('slice', slices)
+    return slices
+}
+
+//转换table的数据格式,写入ppt
+function getTableData (data){
+    //data:[{m:'表格数据',mc:{rs:2,cs:1}}] -> [{text:'表格数据',options:{rowspan:2,colspan:1}}]
+    const tableData = []
+    for(let i=0;i<data.length;i++){
+      const row = data[i]
+      const _row =[]
+      for(let j=0;j<row.length;j++){
+        const cell = row[j]
+        let cellOptions = {
+          colspan:cell.mc.cs===0?1:cell.mc.cs,
+          rowspan:cell.mc.rs===0?1:cell.mc.rs
+        }
+          _row.push({
+            text:cell.m,
+            options:cellOptions
+          })
+  
+      }
+      if(_row.length) tableData.push(_row)
+    }
+    return tableData
+}
+
+//计算各版式下,对应position在ppt中的位置
+function getPosition(modelId, position) {
+    //找到modelConfig[modelId].elments[position]
+    const model = modelConfig.find((i) => i.modelId === modelId);
+    const { x, y, width, height } = model.elements.find(
+      (i) => i.position === position
+    );
+    return {
+        x:x+'%',
+        y:y+14+'%',
+        width:width+'%',
+        height:height+'%'
+    };
+}
+
+//计算图像的真实尺寸(object-fit:scale-down)
+function getImgRealSize ({imgWidth,imgHeight,naturalWidth,naturalHeight}){
+    let ratio = naturalWidth/naturalHeight
+    let width = imgHeight*ratio,height=imgHeight
+    if(width>imgWidth){
+        width = imgWidth
+        height = imgWidth/ratio
+    }
+    return {width,height}
+}
+
+function toHex(n){
+    return `${n > 15 ? '' : 0}${n.toString(16)}`;
+} 
+function toHexString (colorObj){
+   const { r, g, b,} = colorObj;
+   return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
+}
+
+//将rgba转为hex+透明度的格式
+//rgba(91,155,213,0.44) -> {color: "#5b9bd5",transparency: 56}
+function parseRgbaColor (color) {
+    const arr = color.match(/(\d(\.\d+)?)+/g) || [];
+    const res = arr.map((s) => parseInt(s, 10));
+    return {
+        r: res[0],
+        g: res[1],
+        b: res[2],
+        a: parseFloat(arr[3]),
+    } 
+}
+//获取shape的格式数据,写入ppt
+function rgbaToHex (rgba)  {
+    const colorObj = parseRgbaColor(rgba);
+    return {color:toHexString(colorObj),transparency:(1-colorObj.a)*100}
+}
+function getShapeOptions (el,position,scale){
+    let options = {
+      x:position.x+'%',
+      y:position.y+'%',
+      w:position.w+'%',
+      h:position.h+'%',
+    }
+    if(el.type==='shape'&&el.shapeType==='Rect'){
+      options.fill = rgbaToHex(el.fill)
+      options.points=[
+        {x:0,y:0,moveTo:true},
+        {x:position.w+'%',y:0},
+        {x:position.w+'%',y:position.h+'%'},
+        {x:0,y:position.h+'%'},
+        {close:true}
+      ]
+      options.line = {
+        type:'solid',
+        color:rgbaToHex(el.outline.color).color,
+        transparency:rgbaToHex(el.outline.color).transparency,
+        width:el.outline.width,
+        dashType:el.outline.style === 'solid' ? 'solid' : 'dash',
+      }
+    }
+    if(el.type==='line'){
+      const width = Math.max(el.start[0], el.end[0])
+      const height = Math.max(el.start[1], el.end[1])
+      //px->英尺 1英尺≈100px(在ppt-width为10英尺的情况下)
+      //906:628.6(编辑页的宽高比) --> 1008:705.6 相当于扩大了1.13倍
+      options.w = (width*scale.x/100)>10?10:width*scale.x/100
+      options.h = (height*scale.y/100)>10?10:height*scale.y/100
+      options.points = [
+        {x:Math.min(el.start[0]*scale.x/100,10),y:Math.min(el.start[1]*scale.y/100,10),moveTo:true},
+        {x:Math.min(el.end[0]*scale.x/100,10),y:Math.min(el.end[1]*scale.y/100,10)}
+      ]
+      options.line={
+        type:'solid',
+        color:rgbaToHex(el.color).color,
+        transparency:rgbaToHex(el.color).transparency,
+        width:el.strokeWidth*0.75,//不缩小下的话线太粗了
+        dashType:el.style==='solid'?'solid':'dash',
+        beginArrowType: el.points[0] ? 'triangle' : 'none',
+        endArrowType: el.points[1] ? 'triangle' : 'none',
+      }
+    }
+    return options
+}
+
+
+// 生成随机码
+function createRandomCode (len = 8) {
+	const charset = `_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`
+	const maxLen = charset.length
+	let ret = ''
+	for (let i = 0; i < len; i++) {
+	  const randomIndex = Math.floor(Math.random() * maxLen)
+	  ret += charset[randomIndex]
+	}
+	return ret
+}
+
+// 上传到阿里云
+async function handleUploadToOSS(data){
+	// 生成文件名
+	const t=new Date()
+	const month=moment(t).format('YYYYMM')
+	const day=moment(t).format('YYYYMMDD')
+	const temName=`ppt/${month}/${day}/${createRandomCode(32)}.pptx`
+	console.log('文件名',temName);
+	const url=await useUploadFileToOSS(data,temName)
+	if(!url){
+		showToast('生成ppt失败')
+		return
+	}
+	handlePublishPPT(url)
+}
+
+// 页面转ppt
+async function pageToPPT(){
+    const PPTINS=PPTInit(new pptxgen(),1,'ch')
+    PPTINS.addSlide()
+
+    // ppt正文内容
+    for (let i = 1; i < PPTContentList.length-1; i++) {
+        //console.log(`正在生成,第${i+1}页...`,`lastVisibleItemIndex:`,this.lastVisibleItemIndex)
+        
+        let slide = PPTINS.addSlide({ masterName: pptSlideMaster.title });
+        //添加背景图片
+        //slide.background = { path: "/static/pptnextimg.png" };
+        slide.addText(PPTContentList[i].title, {
+            placeholder:"slideTitle",
+            x:'10%',
+            y:'5.5%',
+            w:'68%',
+            h:'7%',
+            color:'333333'
+        });
+        const elements = PPTContentList[i].elements;
+        const elLength = elements.length;
+        for (let j = 0; j < elLength; j++) {
+            let imgData = null,textData = null,imgData2 = null,imgData2Obj=null,sheetData=null;
+            if (elements[j].type === "chart") {
+                console.log("img/chart...");
+                const chartId=`chart${elements[j].chartId}_${i}_${elements[j].position}`
+                // 判断图有没有被删除
+                const chartIsDel=$(`#${chartId} .empty-img`)
+                if(chartIsDel.length==0){
+                    //将svgDom转为base64 png,返回一个base64字符串
+                    imgData = await changeUrl(chartId)
+                }
+            } else if (elements[j].type === 'text'){
+                console.log('text...')
+                textData = toTextProps(toJson(elements[j].richContent))
+            }else if (elements[j].type==='image'){
+                console.log("img/image...")
+                imgData2 = PPTContentList[i].elements[j].src
+                imgData2Obj = {
+                    imgWidth:$(`#image_${i}_${elements[j].position}`).width(),
+                    imgHeight:$(`#image_${i}_${elements[j].position}`).height(),
+                    naturalWidth:document.getElementById(`image_${i}_${elements[j].position}`).naturalWidth,
+                    naturalHeight:document.getElementById(`image_${i}_${elements[j].position}`).naturalHeight
+                }
+            }else if (elements[j].type==='sheet'){
+                console.log('table...')
+                const sheetId = elements[j].sheetId
+                const res=await apiSheetInfoByCode({UniqueCode:sheetId})
+                if(res.Ret===200){
+                    sheetData = getTableData(res.Data?.TableInfo?.TableDataList||[])
+                }
+            }
+            const { x, y, width, height } = getPosition(
+                PPTContentList[i].modelId,
+                elements[j].position
+            );
+            if (imgData) {
+                slide.addImage({
+                    data:imgData,
+                    x: x,
+                    y: y,
+                    w: width,
+                    h: height,
+                    size: { type: "contain" },
+                });
+            }else if (textData){
+                slide.addText(textData,{
+                    x:x,
+                    y:y,
+                    w:width,
+                    h:height,
+                    margin:10,
+                    fontSize: 16*0.75,
+                    valign:'top'
+                })
+            }else if(imgData2){
+                //console.log('src',imgData2)
+                const realSize = getImgRealSize(imgData2Obj)
+                const percentWidth = Number(width.substring(0,width.length-1))
+                const percentHeight = Number(height.substring(0,height.length-1))
+                const offsetX = realSize.width===imgData2Obj.imgWidth?0:(percentWidth-(realSize.width/imgData2Obj.imgWidth*percentWidth))/2
+                const offsetY = realSize.height===imgData2Obj.imgHeight?0:(percentHeight-(realSize.height/imgData2Obj.imgHeight*percentHeight))/2
+                const realX = Number(x.substring(0,x.length-1))+offsetX
+                const realY = Number(y.substring(0,y.length-1))+offsetY
+                /* const realWidth = offsetX===0?width:(realSize.width/imgData2Obj.imgWidth*percentWidth)
+                const realHeight = offsetY===0?height:(realSize.height/imgData2Obj.imgHeight*percentHeight) */
+                //console.log('position x',x,' y',y,' width',width,' height',height)
+                //console.log('x y',realX,realY,'w h',realWidth,realHeight)
+                slide.addImage({
+                    path:imgData2+'?v='+new Date().getTime(),
+                    x:realX+'%',
+                    y:realY+'%',
+                    w:offsetX===0?width:(realSize.width/imgData2Obj.imgWidth*percentWidth)+'%',
+                    h:offsetY===0?height:(realSize.height/imgData2Obj.imgHeight*percentHeight)+'%',
+                    size:{type:"contain"}
+                })
+            }else if(sheetData){
+                slide.addTable(sheetData,{
+                    x:x,
+                    y:y,
+                    w:width,
+                    h:height,
+                    border:{type:'solid',pt:1}
+                })
+            }
+        }
+        //在添加完版式后,添加图层元素
+        const layers = PPTContentList[i].layers||[]
+        const layersLength = layers.length
+        for(let j = 0; j < layersLength; j++){
+            const {percentageTop,percentageLeft,percentageWidth,percentageHeight} = layers[j]
+            //percent单位基于预留标题位置,所以top和height的百分比需要调整
+            let position = {x:percentageLeft*100,y:percentageTop*100*0.86+14,w:percentageWidth*100,h:percentageHeight*100*0.86}
+            //type为shape&line
+            if(['shape','line'].includes(layers[j].type)){
+                // const scale = calcScale({w:906,h:906*0.7},{w:$('.ppt-item').width(),h:$('.ppt-item').width()*this.coefficient})
+                let options = getShapeOptions(layers[j],position,1)
+                //console.log('options',options)
+                slide.addShape('custGeom',options)
+            }
+            //type为text,则转换内部文字
+            if(layers[j].type==='text'){
+                let textData = toTextProps(toJson(layers[j].richContent))
+                    slide.addText(textData,{
+                    x:position.x+'%',
+                    y:position.y+'%',
+                    w:position.w+'%',
+                    h:position.h+'%',
+                    margin:10,
+                    fontSize: 16*0.75,
+                    valign:'top'
+                })
+            }
+        }
+    }
+
+    //添加封底
+    let back = PPTINS.addSlide()
+    let backImg = $(`#ppt-last-page img`)[0].src
+    back.addImage({
+        path: backImg,
+        x: 0,
+        y: 0,
+        w:'100%',
+        h: '100%',
+        size: { type: "contain" },
+    })
+
+    //为了把封面放到第一页,操作pptx.slides达不成想要的效果,于是弄了个pptx2
+    //将封面放在最后生成是因为htmlToCanvans占用太多内存会导致页面假死
+    let pptx2 = PPTInit(new pptxgen(),1);
+    //添加封面
+    let cover = pptx2.addSlide()
+    let coverImg = $(`#ppt-cover-page .pptbg`)[0].src
+    cover.addImage({
+        path: coverImg,
+        x: 0,
+        y: 0,
+        w:'100%',
+        h: '100%',
+        size: { type: "contain" },
+    })
+    //生成的ppt需要可以在封面页更改标题和类型,所以封面信息手动写入
+    const coverInfo = [
+        {text:'—————————————————————————————————\n',options:{fontSize:16*0.75,breakLine:true}},
+        {text:PPTContentList[0].Title,options:{fontSize:28*0.75,breakLine:true}},
+        {text:`\n— 弘则弥道(上海)投资咨询有限公司 ● ${PPTContentList[0].ReportType} —`,
+        options:{fontSize:16*0.75,breakLine:false}},
+        {text:'\nFICC研究部',options:{fontSize:16*0.75,breakLine:true}},
+        {text:PPTContentList[0].PptDate,options:{fontSize:16*0.75,breakLine:true}},
+        {text:'\n—————————————————————————',options:{fontSize:16*0.75,breakLine:true}}
+    ]
+    cover.addText(coverInfo,{
+        x:'38%',
+        y:'50%',
+        w:'60%',
+        h:'28%',
+        color:'ffffff',
+        align:'center',
+        fontFace:'SimHei'
+    })
+    //遍历pptx.slides,重新给每一项的部分属性赋值,再推入pptx2.slides中
+    //第一页不需要,因为是空白的
+    for(let i=1;i<PPTINS.slides.length;i++){
+        let item  = _.cloneDeep(PPTINS.slides[i])
+        item._name = `Slide ${i+1}`
+        item._slideNum = i+1
+        item._rId = cover._rId+i
+        item._slideId  = cover._slideId+i
+        pptx2._slides.push(item)
+    }
+    pptx2.write('blob').then((data)=>{
+		LoadingINS.close()
+		// 上传到阿里云oss
+		handleUploadToOSS(data)
+    })
+    // pptx2.writeFile({ fileName: "test.pptx" }) //本地测试可直接用该方法生成ppt文件
+}
+
+// 发布ppt
+function handlePublishPPT(url){
+	apiPPTPublish({
+		PptId:Number(pptId),
+        PptxUrl:url
+	}).then(res=>{
+		if(res.Ret===200){
+			showToast('发布成功')
+		}
+	})
+}
+
+/**
+ * 发布ppt入口方法
+ * @param data ppt内容list
+ * @param id pptid
+ */
+export async function usePPTPublish(data,id){
+    PPTContentList=data
+	pptId=id
+    if(!checkPPT()) return
+    console.log('校验通过');
+    LoadingINS=showLoadingToast({
+        message: "发布中...",
+        duration: 0,
+        forbidClick: true,
+    })
+    pageToPPT()
+
+
+}

+ 2 - 2
src/views/ppt/style/common.scss

@@ -4,10 +4,10 @@ $pptItemH:630PX;
 .ppt-item-box{
     width: $pptItemW;
     height: $pptItemH;
-    background: url('https://hzstatic.hzinsights.com/static/ppt_m/pptitem_bg.png');
+    background: url('/pptImg/pptitem_bg.png');
     background-size: cover;
     background-repeat: no-repeat;
-    margin-top: 20PX;
+    margin-bottom: 10PX;
     position: relative;
     overflow: hidden;
     // border: 4PX solid transparent;

+ 1 - 1
src/views/ppt/template/Cover.vue

@@ -9,7 +9,7 @@ const props=defineProps({
 </script>
 
 <template>
-    <div class="ppt-item-box ppt-cover-page">
+    <div class="ppt-item-box ppt-cover-page" id="ppt-cover-page">
         <img :src="pageData.imgLocalUrl.image_url" class="pptbg"  style="width:100%"/>
         <div style="width:62%; font-size:16px; text-align:center; line-height:1.6; color:#fff; position:absolute; right:20px; top:50%;zIndex:20;">
             <p style="height:5px; border-top:1px solid #fff;marginBottom:21px;"></p>

+ 1 - 1
src/views/ppt/template/Footer.vue

@@ -9,7 +9,7 @@ const props=defineProps({
 </script>
 
 <template>
-    <div class="ppt-item-box ppt-last-page">
+    <div class="ppt-item-box ppt-last-page" id="ppt-last-page">
         <img :src="pageData.bgImg" alt="">
     </div>
 </template>

+ 252 - 1
src/views/ppt/utils/config.js

@@ -2,7 +2,258 @@
 
 //ppt封面背景图
 export const bgList=[
-    {image_url:'https://hzstatic.hzinsights.com/static/ppt_m/pptcover_bg3.jpg'},
+    {image_url:'/pptImg/pptcover_bg3.jpg'},
     {image_url:'https://hzstatic.hzinsights.com/static/ppt_m/pptcover_bg4.jpg'},
     {image_url:'https://hzstatic.hzinsights.com/static/ppt_m/pptcover_bg5.jpg'},
 ]
+//ppt宽高比
+export const pptLayout = { name: "myppt", width: 10, height: 7 }
+//ppt母版
+export const pptSlideMaster = {
+	title: "幻灯片母版",
+	objects: [
+		{
+			placeholder: {
+				options: {
+					name: "slideTitle",
+					type: "title",
+					x: "10%",
+					y: "5%",
+					w: "68%",
+					h: 7 + "%",
+					color: "333333",
+                    align:'left',
+                    valign:'middle',
+					fontSize:24*0.75,
+				},
+			},
+		},
+		{
+			image: {
+				x: 0,
+				y: 0,
+				w: 10,
+				h: 7,
+				path: "/pptImg/pptitem_bg.png", //会发送http请求,static or 线上地址
+			},
+		},
+	],
+    slideNumber:{x:"95%",y:"94%",fontSize:12}
+}
+//ppt母版英文
+export const pptSlideMasterEn = {
+    title: "幻灯片母版",
+	objects: [
+		{
+			placeholder: {
+				options: {
+					name: "slideTitle",
+					type: "title",
+					x: "8%",
+					y: "5%",
+					w: "62%",
+					h: 7 + "%",
+					color: "333333",
+                    align:'left',
+                    valign:'middle',
+					fontSize:24*0.75,
+				},
+			},
+		},
+		{
+			image: {
+                x: 0,
+                y: -0.1,
+                w: 10,
+                h: 7.1,
+                path: "/pptImg/pptitem_bg.png", 
+            },
+		},
+	],
+    slideNumber:{x:"95%",y:"94%",fontSize:12}
+}
+
+//给标题预留的位置,单位%
+export const marginTop = 14
+const titleHeight = 7
+const restHeight = 100 - marginTop
+//版式位置宽高设置,x,y,width,height都是基于整张ppt的百分比
+//百分比的值来自./css/format.scss ->.chart-wrap,.editor-wrap
+export const modelConfig = [
+    {
+        modelId: 1,
+        elements: [
+            {
+                position: 1,
+                width: 100*0.9, //单位%
+                height: (restHeight)*0.9,
+                x: (100-100*0.9)/2, //单位%,left
+                /* y: ((restHeight)-(restHeight)*0.9)/2, //top */
+                y:1.5
+            }
+        ]
+    }, 
+    {
+        modelId: 2,
+        elements: [{
+            position: 1,
+            width: 100*0.6*0.9, 
+            height:(restHeight)*0.8 ,
+            x: (100*0.6-100*0.6*0.9)/2,
+            y: ((restHeight)-(restHeight)*0.8)/2
+        }, {
+            position: 2,
+            width: 100*0.4*0.9, 
+            height: (restHeight)*0.8,
+            x: 60+(100*0.4-100*0.4*0.9)/2,// or 60
+            y: ((restHeight)-(restHeight)*0.8)/2
+        }]
+    },
+    {
+        modelId: 3,
+        elements: [{
+            position: 1,
+            width: 100*0.5*0.9,
+            height: (restHeight)*0.5*0.84,
+            x: (100*0.5-100*0.5*0.9)/2,
+            y: ((restHeight)*0.5-(restHeight)*0.5*0.84)/2
+        }, {
+            position: 2,
+            width: 100*0.5*0.9,
+            height: (restHeight)*0.5*0.84,
+            x: (100*0.5-100*0.5*0.9)/2,
+            //这个位置的图表布局是align-items: flex-start,所以紧接着上一个图表
+            y: 50-7/* +((restHeight)*0.5-(restHeight)*0.5*0.84)/2 */,//or 50
+        }, {
+            position: 3,
+            width: 100*0.5*0.9,
+            height: (restHeight)*0.84,
+            x: 50,
+            y: ((restHeight)-(restHeight)*0.84)/2
+        }]
+    },
+    {
+        modelId: 4,
+        elements: [{
+            position: 1,
+            width: 100*0.5*0.9,
+            height: (restHeight)*0.5*0.84,
+            x: (100*0.5-100*0.5*0.9)/2,
+            y: ((restHeight)*0.5-(restHeight)*0.5*0.84)/2
+        }, {
+            position: 2,
+            width: 100*0.5*0.9,
+            height: (restHeight)*0.5*0.84,
+            x: (100*0.5-100*0.5*0.9)/2,
+            y: 50-7/* +((restHeight)*0.5-(restHeight)*0.5*0.84)/2 */,//or 50
+        }, {
+            position: 3,
+            width: 100*0.5*0.9,
+            height: (restHeight)*0.5*0.84,
+            x: 50+(100*0.5-100*0.5*0.9)/2,//or 50
+            y: ((restHeight)*0.5-(restHeight)*0.5*0.84)/2
+        }, {
+            position: 4,
+            width: 100*0.5*0.9,
+            height: (restHeight)*0.5*0.84,
+            x: 50+(100*0.5-100*0.5*0.9)/2,
+            y: 50-7/* +((restHeight)*0.5-(restHeight)*0.5*0.84)/2 */,//or 50
+        }]
+    },
+    {
+        modelId: 5,
+        elements: [{
+            position: 1,
+            width: 100*0.5*0.9,
+            height: (restHeight)*0.5*0.84,
+            x: (100*0.5-100*0.5*0.9)/2,
+            y: ((restHeight)*0.5-(restHeight)*0.5*0.84)/2
+        }, {
+            position: 2,
+            width: 100*0.5*0.9,
+            height: (restHeight)*0.5*0.84,
+            x: (100*0.5-100*0.5*0.9)/2,
+            y: 50-7/* +((restHeight)*0.5-(restHeight)*0.5*0.84)/2 */,//or 50
+        }, {
+            position: 3,
+            width: 100*0.5*0.9,
+            height: (restHeight)*0.5*0.84,
+            x: 50+(100*0.5-100*0.5*0.9)/2,//or 50
+            y: ((restHeight)*0.5-(restHeight)*0.5*0.84)/2
+        }, {
+            position: 4,
+            width: 100*0.5*0.9,
+            height: (restHeight)*0.5*0.84,
+            x: 50+(100*0.5-100*0.5*0.9)/2,
+            y: 50-7/* +((restHeight)*0.5-(restHeight)*0.5*0.84)/2 */,//or 50
+        }]
+    },
+    {
+        modelId: 6,
+        elements: [{
+            position: 1,
+            width: 100*0.9,
+            height: (100-marginTop)*0.9,
+            x: (100-100*0.9)/2, 
+            y: ((100-marginTop)-(100-marginTop)*0.9)/2, 
+        }]
+    },
+    {
+        modelId:7,
+        elements:[{
+            position:1,
+            width:100*0.5*0.9,
+            height:(restHeight)*0.8,
+            x:(100*0.5 - 100*0.5*0.9)/2,
+            y:((restHeight)-(restHeight)*0.8)/2
+        },{
+            position:2,
+            width:100*0.5*0.9,
+            height:(restHeight)*0.8,
+            x:50+(100*0.5 - 100*0.5*0.9)/2,
+            y:((restHeight)-(restHeight)*0.8)/2
+        }]
+    },
+    {
+    modelId:8,
+    elements:[{
+        position:1,
+        width:100*0.9,
+        height:(restHeight)*0.7*0.9,
+        x:(100-100*0.9)/2,
+        y:(restHeight*0.7-restHeight*0.7*0.9)/2
+    },
+    {
+        position:2,
+        width:100*0.9,
+        height:(restHeight)*0.3*0.8,
+        x:(100-100*0.9)/2,
+        y:60
+    }]
+    },
+    {
+    modelId:9,
+    elements:[{
+        position:1,
+        width:100*0.5*0.9,
+        height:(restHeight)*0.7*0.9,
+        x:(100*0.5-100*0.5*0.9)/2,
+        y:(restHeight*0.7-restHeight*0.7*0.9)/2
+    },
+    {
+        position:2,
+        width:100*0.5*0.9,
+        height:(restHeight)*0.7*0.9,
+        x:50+(100*0.5-100*0.5*0.9)/2,
+        y:(restHeight*0.7-restHeight*0.7*0.9)/2
+    },
+    {
+        position:3,
+        width:100*0.9,
+        height:(restHeight)*0.3*0.8,
+        x:(100-100*0.9)/2,
+        y:60
+    }
+    ]
+    }
+]

Some files were not shown because too many files changed in this diff