Browse Source

feat: 导出pdf

lvkun996 1 year ago
parent
commit
4b842f84d9

+ 22 - 0
copy-plugins.js

@@ -0,0 +1,22 @@
+
+/**
+ * @description 有些依赖需要手动添加,防止复制粘贴
+ * @author lvkun
+ */
+
+const { resolve } = require("path");
+
+const fs = require("fs");
+
+const sorcePath = "src/static";
+
+const targetPath = './dist/static'
+
+fs.mkdirSync(resolve(targetPath));
+
+const files = fs.readdirSync(`./${sorcePath}`);
+
+files.forEach( file => {
+   const _file = fs.readFileSync(resolve(sorcePath, file));
+   fs.writeFileSync(resolve(targetPath, file), file);
+})

BIN
dist.zip


File diff suppressed because it is too large
+ 723 - 20
package-lock.json


+ 2 - 1
package.json

@@ -4,7 +4,7 @@
   "version": "0.0.0",
   "scripts": {
     "dev": "vite",
-    "build": "vue-tsc && vite build",
+    "build": "vue-tsc && vite build && node copy-plugins.js",
     "preview": "vite preview",
     "format": "prettier --write src/",
     "upload:pro": "python upload.py /usr/share/nginx/html/luojigou/ report pro",
@@ -16,6 +16,7 @@
     "jspdf": "^2.5.1",
     "lottie-web": "^5.12.2",
     "pdfjs-dist": "^4.0.189",
+    "pdfmake": "^0.3.0-beta.1",
     "pinia": "^2.1.6",
     "sass": "^1.67.0",
     "sass-loader": "^13.3.2",

BIN
src/assets/images/semester_growth_logo.png


BIN
src/assets/pdf/font/STXINGKA.TTF


BIN
src/assets/pdf/font/SourceHanSansCN-Bold.otf


BIN
src/assets/pdf/font/SourceHanSansCN-Regular.otf


File diff suppressed because it is too large
+ 7 - 0
src/assets/pdf/fonta.js


File diff suppressed because it is too large
+ 10909 - 0
src/assets/pdf/pdfmake.js


BIN
src/static/STXINGKA.TTF


BIN
src/static/SourceHanSansCN-Bold.otf


BIN
src/static/SourceHanSansCN-Regular.otf


+ 2 - 0
src/types/customize.d.ts

@@ -148,6 +148,8 @@ interface IDomainData {
     abilityName: string;
     abilityIconUrl: string;
     story: IStory;
+    growth: string
+    tactics: string
     education: string;
   }[];
 }

+ 471 - 0
src/utils/exportPdf.js

@@ -0,0 +1,471 @@
+import pdfMake from "pdfmake/build/pdfmake";
+import pdfFonts from "pdfmake/build/vfs_fonts";
+
+
+import Regular from '@/assets/pdf/font/SourceHanSansCN-Regular.otf'
+import Bold from '@/assets/pdf/font/SourceHanSansCN-Bold.otf'
+import STXINGKA from '@/assets/pdf/font/STXINGKA.TTF'
+
+export const exportPDF = async (_semesterReport) => {
+
+   const semesterReport = JSON.parse(JSON.stringify(_semesterReport))
+
+  const createPdfContent = () => {  
+
+    const _data = semesterReport
+
+    const H1 = 30
+
+    const H2 = 20
+
+    const H3 = 14
+
+    const H4 = 12
+    
+    const H5 = 10
+    
+    const brakePage = {
+        text: " ",
+        margin: [0, 0, 0, 600]
+    }
+
+    const H1Template = (text) => (
+        { text: `${text}`, 
+        fontSize: H1, 
+        margin: [0, 40, 0, 15], 
+        color: "blue",  
+        bold: true, 
+        font: 'STXINGKA.TTF', 
+        alignment: 'center'
+    })
+
+    const H2Template = (text) => (
+        { text: `${text}`, 
+        color: '#1a2c5e',
+        fontSize: H2, 
+        margin: [0, 15, 0, 15],
+        bold: true
+    })
+
+    const H3Template = (text) => (
+        { text: `${text}`, 
+        fontSize: H3, 
+        color: '#1a2c5e',
+        margin: [0, 10, 0, 10],
+        bold: true
+    })
+
+    
+    const H3DotTemplate = (text) => (
+        {
+            text: '',
+
+        },
+        {
+            columns: [
+                {
+                    image: 'deg',
+                    width: 6,
+                    margin: [0, 5, 0, 10],
+                },
+                {   text: `${text}`, 
+                    fontSize: H3, 
+                    // color: '#1a2c5e',
+                    margin: [5, 0, 0, 10],
+                    bold: true
+                }
+            ]
+        }
+       )
+
+    
+    const H4Template = (text) => (
+        { text: `${text}`, 
+        fontSize: H4,
+        color: '#1a2c5e',
+        margin: [0, 15, 0, 15],
+    }) 
+
+    const H5Template = (text) => (
+        { text: `${text}`, 
+        fontSize: H5,
+        margin: [0, 15, 0, 15],
+        lineHeight: 2
+    }) 
+
+    const H5ColorTemplate =  (title, text) => (
+        {
+            columns: [
+                {
+                    image: 'deg',
+                    width: 6
+                },
+                { text: `${title}`, 
+                    fontSize: H5,
+                    color: '#2a69fd',
+                    width: 55,
+                    margin: [5, -2, 0, 0],
+                },
+                { text: `${text}`, 
+                    fontSize: H5,
+                    margin: [5, -2, 0, 0],
+                }
+            ]
+        }
+    ) 
+
+
+    const qrTemplate = (url) => ({ qr: url, fit: '120', alignment: 'center'})
+
+    const textTemplate = (text) => ({ text: `${text}`, fontSize: H4, margin: [0, 15, 0, 15] })
+
+    const imgTemplate = (url) => ({image: url, width: 160, alignment: 'center', margin: [0, 30, 0, 30] } )
+
+    const caclImgSplit = (arr) => {
+            if (arr.length === 0 ) return [{}]
+
+            let lastValue = null
+
+            if (arr.length % 2 !== 0 && arr.length >= 3 ) {
+                lastValue = arr.splice(arr.length - 1, 1)
+            }
+
+            if (arr.length === 1) return [{...imgTemplate(arr[0])}]
+
+            const _arr = [[]]
+
+            arr.forEach( item => {
+                const lastIndex = _arr.length - 1
+                if (_arr[lastIndex].length < 2) {
+                    _arr[lastIndex].push(item)
+                } else {
+                    _arr.push([])
+                    _arr[_arr.length - 1].push(item)
+                }
+            })  
+   
+            const r = _arr.map( imgs => {
+                return  {
+                    columns: [
+                      {
+                        text: "",
+                        width: 82
+                      },
+                      imgTemplate(imgs[0]),
+                       {
+                        text: "",
+                        width: 30
+                      },
+                      imgTemplate(imgs[1])
+                    ]
+                }
+                } 
+            )
+            if (lastValue != null) r.push(imgTemplate(lastValue[0]))
+            return r
+    }
+
+    const calcVideoSplit = (videos) => {
+        return videos.map( video => {
+            return [qrTemplate(video), {...H5Template('视频扫码查看'), alignment: 'center'}]
+        }).flat(2)
+    }
+
+    const cover =  {
+        image: 'cover',
+        width: 450,
+        alignment: 'center'
+    }
+
+    const ava = {
+        image: _data.babyHeadImg,
+        width: 200,
+        alignment: 'center',
+        margin: [0, 50,20, 0]
+    }
+
+    const coverInfo = {
+        text: `姓名:${_data.babyName}\n\n班级:${_data.className}\n\n成长阶段:${_data.startDate}-${_data.endDate}`,
+        alignment: 'center',
+        margin: [0, 50, 0, 200],
+        font: 'STXINGKA.TTF',
+        fontSize: 26
+    }
+
+    const userInfo = [
+        {
+            text: `姓名:${_data.babyName}`,
+            alignment: 'center',
+            font: 'STXINGKA.TTF',
+            margin: [0, 0, 0, 0],
+            bold: true,
+            fontSize: 24
+        },
+        {
+            text: `年龄:${_data.age}`,
+            margin: [0, 20, 0, 20],
+            alignment: 'center',
+            bold: true,
+            font: 'STXINGKA.TTF',
+            fontSize: 24
+        },
+        {
+            text: `我的幼儿园:\n${_data.schoolName}`,
+            margin: [0, 20, 0, 20],
+            alignment: 'center',
+            bold: true,
+            font: 'STXINGKA.TTF',
+            fontSize: 24
+        },
+        {
+            text: `我的爱好:\n${_data.hobby}`,
+            margin: [0, 20, 0, 20],
+            alignment: 'center',
+            bold: true,
+            font: 'STXINGKA.TTF',
+            fontSize: 24
+        },
+        
+        {
+            text: `我的老师:\n${_data.teachers.join('、')}`,
+            // text: `我的老师:\n${_data.teachers.slice(0, 2)}`,
+            margin: [0, 20, 0, 600],
+            alignment: 'center',
+            font: 'STXINGKA.TTF',
+            bold: true,
+            fontSize: 24
+        },
+    ]
+        
+    const learnDepthList = [
+        H1Template("综合发展情况"),
+        H2Template("高阶思维评估概括"),
+        textTemplate(_data.learnDepthList.join('、')),
+        H2Template("幼儿分领域评估概括"),
+        ..._data.domainAbilityNameList.map( item => {
+            return [ H3DotTemplate(item.domainName), H5Template('评估了 ' +  item.abilityNameList.join('、')  + item.abilityNameList.length + ' 个能力' ) ]
+        })  
+    ]
+
+    const domainAbilityNameList = [H1Template("分领域能力评估概括")]
+
+    _data.domainAbilityNameList.forEach(item => {
+        domainAbilityNameList.push(H2Template(item.domainName))
+        domainAbilityNameList.push(textTemplate(item.abilityNameList.join('、')))
+    })
+
+    domainAbilityNameList.push(brakePage)
+        
+    const questionList = [
+        H1Template("问题探究"),
+        ..._data.questionList.map( item => {
+
+
+            
+
+            const r = item.questions.map( (question, index) => {
+               
+                const date = {...H5ColorTemplate('记录日期:', question.recordDate),  margin: [0, -2, 0, 15],} //  + question.recordDate
+ 
+                const activity =  H5ColorTemplate('活动形式:' , question.activityCategories.join('、')) // + question.activityCategories.join('、')
+                // const columns = {
+                //     columns: [H4Template('记录日期:' + question.recordDate), H4Template('活动形式:' + question.activityCategories.join('、'))]
+                // }
+                const title1 = H4Template('问题记录:')
+                const quesSection = H5Template(`问题${index + 1}: ${question.question}`)
+                const title2 = H2Template('探究过程')
+                
+                const  plan = H3DotTemplate('计划')
+                
+                const  planContent = H5Template(question.plan.content)
+
+                const planImgs = caclImgSplit(question.plan.images)
+
+                const planVideos = calcVideoSplit(question.plan.videos)
+
+                const practice = H3DotTemplate('实施')
+
+                const practiceContent = H5Template(question.practice.content)
+
+                const practiceImgs = caclImgSplit(question.practice.images)
+
+                const practiceVideos = calcVideoSplit(question.practice.videos)
+
+                const summary = H3DotTemplate('总结与反思')
+
+                const summaryContent = H5Template(question.summary.content)
+                
+                const summaryImgs = caclImgSplit(question.summary.images)
+
+                const summaryVideos = calcVideoSplit(question.summary.videos)
+
+                const tweaks = H3DotTemplate('调整')
+
+                const tweaksContent = H5Template(question.tweaks.content)
+
+                const tweaksImgs =  caclImgSplit(question.tweaks.images)
+
+                const tweaksVideos = calcVideoSplit(question.tweaks.videos)
+
+                // babyResult
+                const baby = H3Template('高阶思维评估')
+
+                const bbContent = []
+                
+                question.babyResults.forEach( bb => {
+
+                   bbContent.push(H4Template('记录对象:' + bb.babyName))
+                
+                   bb.list.forEach( b => {
+                        bbContent.push(H4Template('·' +  b.learnDepth + b.levelCode))
+                        bbContent.push(H5Template(b.levelText))
+                    })
+               
+                })
+                
+                return [
+                    date, activity, title1, quesSection, title2, plan, planContent, ...planImgs,  ...planVideos, 
+                    practice, practiceContent, ...practiceImgs, ...practiceVideos, summary, summaryContent, ...summaryImgs, ...summaryVideos, tweaks, tweaksContent, ...tweaksImgs ,
+                    ...tweaksVideos, baby, ...bbContent
+                ]
+            })
+
+            return [
+                
+             
+                H2Template( `${item.topic}`),
+                H5ColorTemplate(`记录教师:`, item.teacherName),
+                H4Template( `${item.background}`),
+                ...r
+            ]
+        })
+    ]
+   
+    const domainDataList = [
+        H1Template('行为记录'),
+        ..._data.domainDataList.map( item => {
+            const title = H2Template(item.domainName)
+         
+            const recordList = []
+
+            item.recordList.map( record => {
+                
+                recordList.push(H4Template('记录日期:' + record.recordDate))
+                recordList.push(H4Template('观察实录:'))
+                recordList.push(caclImgSplit(record.story.images))
+                recordList.push(H4Template(record.abilityName))
+                recordList.push(H4Template('进阶能力描述:'))
+                recordList.push(H4Template(record.growth))
+                recordList.push(H4Template('家园共育策略'))
+                recordList.push(H5ColorTemplate('一日活动', ''))
+                recordList.push(H5Template(record.tactics))
+                recordList.push(H5ColorTemplate('家园共育', ''))
+                recordList.push(H5Template(record.education))
+
+            })  
+
+            return [title,  ...recordList]
+        }).flat(2)
+    ]
+
+
+    return [cover, ava, coverInfo, { text: ' ', margin: [0, 0, 0, 120] },  ...userInfo, ...learnDepthList, brakePage, ...questionList.flat(2 ), brakePage, ...domainDataList]
+
+    }
+
+
+    const findImgs = () => {
+                  
+      const question = semesterReport.questionList
+      const domainDataList = semesterReport.domainDataList
+
+      const r = {
+        [semesterReport.babyHeadImg]: semesterReport.babyHeadImg,
+      }
+
+
+      question.forEach(item => {
+          item.questions.forEach(_ => {
+              _.plan.images.forEach( img => r[img ] = img + '?imageMogr2/thumbnail/!72p' )
+              _.practice.images.forEach( img => r[ img] = img + '?imageMogr2/thumbnail/!72p')
+              _.summary.images.forEach( img => r[img ] = img + '?imageMogr2/thumbnail/!72p')
+              _.tweaks.images.forEach( img => r[img] = img + '?imageMogr2/thumbnail/!72p' )
+          }
+          )
+      })
+
+      domainDataList.forEach(item => {
+          item.recordList.forEach( record => {
+              record.story.images.forEach( image => r[image] = image + '?imageMogr2/thumbnail/!72p' )
+          })
+          
+      })
+
+
+      return r 
+    }
+  
+    async function generate() {
+
+      const pdfContent = {
+          pageBreakBefore: false,
+          content: null,
+          images: {
+              ...findImgs(),
+              cover:  "https://img.luojigou.vip/cover.png",
+              deg: "https://img.luojigou.vip/FvpoQUenV0XVm1U_DxONnfBIq0nR.png",
+              dot: "https://img.luojigou.vip/FjMe6awMO18PnKxhs5mHnmmvUKrt.png",
+          
+          },
+          defaultStyle: {
+              font: 'SourceHanSans.ttf'
+          },
+          pageSize: 'A4',
+          pageMargins: [40, 30, 40, 30],
+          footer: function (currentPage, pageCount) {
+              return [
+                  { text: currentPage.toString() + ' / ' + pageCount, alignment: 'center' }
+              ]
+          }
+      };
+
+    
+        pdfContent.content = createPdfContent()
+        console.log(pdfContent.content );
+        pdfMake.createPdf(pdfContent).download();
+    }
+
+
+    const isDev = import.meta.env.MODE === 'development'
+
+    const prefix = isDev ? location.origin + '/src/static' : `https://luojigou.vip/report/static`
+
+    pdfMake.fonts = isDev?  {
+        "SourceHanSans.ttf": {
+            normal: prefix + '/SourceHanSansCN-Regular.otf',
+            bold: prefix + '/SourceHanSansCN-Bold.otf',
+            italics: prefix+ '/SourceHanSansCN-Regular.otf',
+            bolditalics:prefix + '/SourceHanSansCN-Bold.otf'
+        },
+        "STXINGKA.TTF": {
+            normal:  prefix + '/STXINGKA.TTF',
+            bold:  prefix + '/STXINGKA.TTF',
+            italics: prefix + '/STXINGKA.TTF',
+            bolditalics:  prefix + '/STXINGKA.TTF',
+        }
+     } : {
+            "SourceHanSans.ttf": {
+                normal: Regular,
+                bold: Bold,
+                italics: Regular,
+                bolditalics: Bold
+            },
+            "STXINGKA.TTF": {
+                normal: STXINGKA,
+                bold:  STXINGKA,
+                italics: STXINGKA,
+                bolditalics: STXINGKA,
+            }
+        }
+    generate()
+}

BIN
src/utils/pdf/font/STXINGKA.TTF


BIN
src/utils/pdf/font/SourceHanSansCN-Bold.otf


BIN
src/utils/pdf/font/SourceHanSansCN-Regular.otf


File diff suppressed because it is too large
+ 7 - 0
src/utils/pdf/fonta.js


File diff suppressed because it is too large
+ 10909 - 0
src/utils/pdf/pdfmake.js


+ 29 - 12
src/views/customize/SemesterReport.vue

@@ -5,6 +5,8 @@ import { useCustomizeStore } from "@/store";
 // import { storeToRefs } from "pinia";
 import { computed, onMounted, onUnmounted, ref, watch } from "vue";
 import ShareModal from "@/views/customize/components/ShareModal.vue";
+// @ts-ignore
+import { exportPDF } from '@/utils/exportPdf'
 
 const {
   b: babyId,
@@ -141,6 +143,11 @@ function getTargetStyle(index: number) {
   return style;
 }
 
+
+const exportReport = () => {
+  exportPDF(semesterReport.value)
+}
+
 onMounted(async () => {
   if (
     typeof babyId === "string" &&
@@ -150,11 +157,6 @@ onMounted(async () => {
   )
     await getSemesterRecord({ babyId, classId, classLevelCode, semesterType });
 
-  // setTimeout(() => {
-  //   // todo
-  //   downloadPDF("screenshot");
-  //   downloadScreenshotFn("screenshot");
-  // }, 1000);
 
   document.addEventListener("scroll", handleScroll);
 });
@@ -239,6 +241,12 @@ onUnmounted(() => {
       <img class="rd-baby-icon" :src="getImageUrl('baby-icon')" alt="">
 
     </div>
+
+    <div class="rd-logo"  >
+      <img :src="getImageUrl('semester_growth_logo')" alt="" class="rd-logo-img" />
+      <div class="rd-logo-text">综合发展情况</div>
+    </div>
+
     <!--学习维度-->
     <div class="rd-dimension grid_bgi" v-if="semesterReport.learnDepthList && semesterReport.learnDepthList.length > 0">
       <!-- <img :src="getImageUrl('dimension_logo')" alt="" class="rd-dimension-logo" /> -->
@@ -273,11 +281,9 @@ onUnmounted(() => {
       </div>
     </div>
 
-
-
     <!--高阶思维评估-->
 
-    <div class="rd-logo"  >
+    <div class="rd-logo" v-if="semesterReport!.questionList && semesterReport!.questionList.length > 0"  >
       <img :src="getImageUrl('semester_record_logo')" alt="" class="rd-logo-img" />
       <div class="rd-logo-text">问题探究</div>
     </div>
@@ -433,10 +439,20 @@ onUnmounted(() => {
                 {{ ability.story.content }}
               </div>
             </div>
+            <img :src="getImageUrl('desc_name_01')" alt="" class="ability-name" />
+            <div class="ability-content">
+             
+              <div class="ability-content-text">
+                {{ ability.growth }}
+              </div>
+            </div>
             <img :src="getImageUrl('family_title')" alt="" class="ability-name" />
             <div class="ability-family">
-              <div v-for="(f, fIndex) in formatFamily(ability.education)" :key="fIndex" class="ability-family-item">
-                {{ f }}
+              <!-- (f, fIndex) in formatFamily(ability.education) -->
+              <div  class="ability-family-item"> 一日活动  </div>
+              <div style="font-size: 14px;margin-top: 6px;" v-for="(f, fIndex) in formatFamily(ability.tactics)" :key="fIndex" >{{f}}  </div>
+              <div  class="ability-family-item" style="margin-top: 6px;"> 家园共育  </div>
+              <div style="font-size: 14px;margin-top: 6px;" v-for="(f, index) in formatFamily(ability.education)" :key="index">   {{f}}
               </div>
             </div>
           </div>
@@ -447,6 +463,7 @@ onUnmounted(() => {
       <img :src="getImageUrl('small_triangle')" alt="" class="rd-modal-logo" />
       <div class="rd-modal-item flex-center" @click="share">分享</div>
       <div v-if="!semesterReport.sendReport && !isParent" class="rd-modal-item flex-center" @click="send">发送家长</div>
+      <div  class="rd-modal-item flex-center" @click="exportReport">导出报告</div>
     </div>
   </div>
 
@@ -1455,11 +1472,11 @@ onUnmounted(() => {
         &-item {
           position: relative;
           width: 265px;
-          font-size: 14px;
+          font-size: 16px;
           font-family:
             PingFang SC-Regular,
             PingFang SC;
-          font-weight: 400;
+          font-weight: 600;
           color: #1a2c5e;
           line-height: 20px;
 

+ 1 - 1
tsconfig.json

@@ -25,6 +25,6 @@
     "noFallthroughCasesInSwitch": true,
     "allowSyntheticDefaultImports": true
   },
-  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
+  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "src/utils/exportPdf.js"],
   "references": [{ "path": "./tsconfig.node.json" }]
 }

+ 2 - 0
upload.py

@@ -88,6 +88,8 @@ time.sleep(1)
 
 cmd1 = "unzip -o -d " + remote_file_name + " " + remote_file_name + "/dist.zip"
 
+print('服务器上解压压缩包:', cmd1)
+
 stdin, stdout, stderr= ssh.exec_command(cmd1)
 
 result = stdout.read()

+ 1 - 0
vite.config.ts

@@ -46,6 +46,7 @@ export default defineConfig({
       },
     },
   },
+  assetsInclude: ['**/*.TTF', '**/*.otf', 'SourceHanSansCN-Bold.otf', 'SourceHanSansCN-Regular.otf', 'STXINGKA.TTF'],
   server: {
     host: true,
     port: 8989,

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