打印100种格式迥异的医用图文报告单——1周的时间有点长

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!javascript

  • 📢欢迎点赞 :👍 收藏 ⭐留言 📝 若有错误敬请指正,赐人玫瑰,手留余香!
  • 📢本文做者:由webmote 原创,首发于 【掘金】
  • 📢做者格言: 生活在于折腾,当你不折腾生活时,生活就开始折腾你,让咱们一块儿加油!💪💪💪

🎏 序言

掘友们,你们好,我又来了。🕺🕺🕺css

你们在工做中最烦恼的是什么? 是否是重复作相似的工做啊?你有过设计报告作到吐的感觉吗?html

是的,最近我碰上了个大麻烦🥺,Ctrl +C、V键快被我敲掉了。前端

它就是制做价值XX w💰💰💰(听说具体数字容易被举报,这里用XX替换)的某医院用报告单。 该项目主要作心理问卷,而后根据问卷、经后台算法后解析出报告。因为处理的是各类类型的心理体检报告单,因此花样繁多,总共有100+ 的不一样报告须要展现和打印。接手项目的时候,对这些报告单算是懵懂无知,看了几个感受大同小异,就误觉得差很少都相似。vue

还好报酬足够丰厚,要否则对不起我这快要敲坏的手,看我二指禅✌️。java

通过叮叮当当一阵响的脚手架、环境的准备,我以我最快的速度搞定了基本数据的增删改查工做(感谢 vue-element-admin项目),是时候表演制做报告的拿手绝活了。webpack

🎏 01.等等,让我炫个技

最最核心💕的也就是图文混排报告了,先秀秀效果。git

💫第一方队是问卷调查报告和艾森克个性测验报告。github

4.png

💫第二方队是明尼苏达多相人格调查表评估报告。 1.pngweb

💫第三方队是多项人格调查表评估报告。 2.png 但愿能给新手以启迪💏,让老手有东西吐槽💏。

💫第四方队是...

打住!后面的方队都回去吧,领导不审阅了,都擦球很少的样子。

🎏 02.使用三方技术大汇总

一篇图文混排可打印报告单的技术实现,主要涉及到的技术是表格🏢、各种图📊、📈、各种报告块📃、打印🖨️。

轮子虽然也要造,但咱们选择站在巨人的肩膀上造轮子,毕竟站得高看得远,能省一点是一点。

下面列下使用的三方库或包:

src=http___cdn.geekdigging.com_data_analysis_data_visualization_pyecharts_3.gif&refer=http___cdn.geekdigging.gif

  • 封装后在vue内直接和Echart交互: vue-echarts
  • 可打印Echart图的print.js 脚本,具体谁写的也不知道了,没有留版权信息,有须要的童鞋能够留言。
  • 很强大的打印脚本 print-js ,我并没用,听说最新版也支持 echart;

其核心思想是把打印的dom输出到iframe内,并枚举canvas,转换成image。

getHtml: function () {
     ... //这里仅贴部分代码
     //canvass echars图表转为图片
     for (var k4 = 0; k4 < canvass.length; k4++) {
       var imageURL = canvass[k4].toDataURL("image/png");
       var img = document.createElement("img");
       img.src = imageURL;
       img.setAttribute('style', 'max-width: 100%;');
       img.className = 'isNeedRemove'
       // canvass[k4].style.display = 'none'
       // canvass[k4].parentNode.style.width = '100%'
       // canvass[k4].parentNode.style.textAlign = 'center'
       canvass[k4].parentNode.insertBefore(img,canvass[k4].nextElementSibling);
     }
     //作分页
     //style="page-break-after: always"
     var pages = document.querySelectorAll('.result');
     for (var k5 = 0; k5 < pages.length; k5++) {
       pages[k5].setAttribute('style', 'page-break-after: always');
     }
     return this.dom.outerHTML;
   },
复制代码

🎏 03.你的报告实现思路?

小伙子,来,姨给你社(说)句话...

住过西安城中村(吉祥村)的娃都应该听过这个段子。

如今活来了,⚡你摊上事了⚡。

需求: 制做报告,每种报告都须要处理不一样的数据,展现不一样的格式;

image.png

往下看以前,不妨留给本身5分钟⏱️思考时间,看看咱们的实现有哪些差别?

金樽清酒斗十千,玉盘珍羞直万钱。🥂🥂🥂
停杯投箸不能食,拔剑四顾心茫然。🤺🤺🤺
欲渡黄河冰塞川,将登太行雪满山。🚶‍♀️🚶‍♀️🚶‍
闲来垂钓碧溪上,忽复乘舟梦日边。🎣🎣🎣
行路难,行路难,多歧路,今安在?🚶‍♂️🚶‍♂️🚶‍
长风破浪会有时,直挂云帆济沧海。🏄🏄🏄

🎏 03.1 动态模板方案

所谓“动态模板方案”,就是按照报告类型定制该类型的模板组件。

咱们只须要判断模板类型,而后加载相应模板进行渲染,就搞定了这个需求,是否是超简单?

看下代码组织形式:

image.png

  • WQReport/index.vue 是报告的父组件,利用 slot加载模板
  • WQReport/reportTemplate.vue 负责加载动态模板
  • templates 文件夹内就是100个模板定义
  • templates/default 为默认模板,用来兜底,万一找不到模板就用它

03.1.1 WQReport/index.vue 内容

<template>
 <div ref="wrap" class="form-wrap"> <div class="form-content-wrap"> <div ref="print" class="reportBorder"> <div id="print" class="reportBlock"> <slot name="print" /> </div> <div class="footer" /> </div> </div> </div>
</template>

<script> export default { name: 'WqPageReport', data() { return { } }, } </script>
复制代码

03.1.2 WQReport/reportTemplate.vue 内容

这里利用vue的 动态组件 component 技术进行加载动态模板。

而且利用计算属性 loader 来返回加载组件的 Promise。 注意须要使用 require(./templates/${this.type}).default 完成载入。

载入失败了,就返回 this.rptType = () => import(./templates/default)默认模板。

固然数据须要赋值给模板组件的属性data。

<template>
  <div class="theRpt"> <component :is="rptType" v-if="rptType" ref="theRpt" :data="rptData" :type="type" /> </div>
</template>

<script> export default { name: 'ReportTemplate', props: ['rptData', 'type'], data() { return { rptType: null } }, computed: { loader() { if (!this.type) { return null } return () => Promise.resolve(require(`./templates/${this.type}`).default) } }, mounted() { this.loader() .then(() => { console.log('load template:' + this.type) this.rptType = () => this.loader() }) .catch(() => { console.log('load template failed.' + this.type) this.rptType = () => import(`./templates/default`) }) } } </script>
复制代码

03.1.3 templates/t0-000 内容

报告模板的内容较多,这里会简化一部分html代码。

<template>
  <div :id="id" class="template">
    <div style=" width: 100%; " >
      {{ data.SCALE_NAME }}评估报告单      
    </div>
    <div style=" width: 100%; " >
      <div style="width: 90%;">{{ data.REPORT_ID }}</div>
    </div>
    <div style="width: 100%; text-align: center; margin: 30px 0;">
      <table style=" width: 90%; " >
        <tr>
          <td style="width: 12%; text-align: right; font-weight: 800;">姓名:</td>
          <td style="width: 12%; text-align: left;">{{ data.USER_REAL_NAME }}</td>
          <td style="width: 12%; text-align: right; font-weight: 800;">性别:</td>
          <td style="width: 12%; text-align: left;">{{ data.USER_SEX }}</td>
          <td style="width: 12%; text-align: right; font-weight: 800;">年龄:</td>
        </tr>        
      </table>
    </div>
    <div style="width: 100%; text-align: center;">
      <div style=" width: 90%; " >
        {{ data.SCALE_EXPLAIN }}
      </div>
    </div>    
    ... ...
      本评定表最终解释权由临床医师和心理测评专家做出。    
  </div>
</template>

<script> export default { name: 't0000', props: { data: { type: Object, default: () => { return {} } }, type: String }, data() { return { id: `template-${this.type}` } }, created() { console.log('subcom:' + this.type) } } </script>

复制代码

03.1.4 使用报告组件

使用动态模板报告组件,就很容易了。

import wqPageReport from "@/components/WQReport/index";
import rptTemplate from '@/components/WQReport/reportTemplate'

//增长组件引用
components: { wqPageReport, rptTemplate },

//增长模板代码
<wq-page-report ref="form"> <div slot="print" class="printContent"> <rpt-template :type="template" :rpt-data="rptData" /> </div> </wq-page-report>

复制代码

03.1.5 有啥不妥吗?

100个模板我已经Ctrl+C、V完了,命名也都改了一遍。

只等按照报告类型,逐一修改每一个模板的html定义,以及渲染显示实现了。

天,还有渲染显示的逻辑呢!!!✨这,真的要把手敲断啊?✨

每一个报告有一部分是类似的,好比我的资料,签名提示等,这些就算都作成组件,我也得100个模板一个个复制过去啊!

😂我已经哭晕在厕所了😂,钱真尼玛很差挣~~ 我退出好很差?

我感受本身已经上了梁山,下不来了。

而且我感受打包速度有点慢,利用 webpack-bundle-analyzer 插件扫描了下代码,templates模板文件夹所占性能比重超大! 100个模板组件不是盖的~~

报告类型太多了,必须换方案,要不这重复的报告拷贝来拷贝去烦都烦死了。🥺

🎏 03.2 动态配置方案

喝杯白开水,🧺闭目养神10分钟。

好了,冷静事后,加油, webmote!

重要的时刻须要冷静下来,而后再开动脑筋

先绘制下图。

image.png

抽象一下: 每一个报告都由不一样的组件按照顺序结构排列而成。

顺序结构能够看数组,不一样的组件可能会有不一样的属性定义,那么若是使用配置来定义一个报告,能够定义以下结构:

't0-000': [{},{},{}],
't0-001': [{},{},{}],
't0-002': [{},{},{}],
...
复制代码

先看看能不能解决方案1的问题🔥?

若是t0-100的报告格式和t0-002的报告格式类似,则能够复制配置,看起来这个工做量是可控的。

{},组件的属性是什么鬼东西呢?

嗯,咱们暂且不要抽象,用到一个具体组件时在定义不迟。

既然已经由了初步的构思,那让咱们先实现默认报告配置吧!

03.2.1 改造1方案

  • 复用 WQReport/index.vue ,因其模板再slot内,所以无需改动代码
  • 改造 WQReport/reportTemplate.vue 按照配置方案依次渲染相应的组件

报告使用代码:

<wq-page-report ref="form">
      <div slot="print" class="printContent"> <rpt-template :type="template" :rpt-data="rptData" :report="report" :st="theSt" :config="theConfig" /> </div>
    </wq-page-report>
复制代码

这里咱们增长了属性 theConfig,表示某类型报告的配置; theSt,某类型报告配置相关联的数据, report,报告的详细原始数据,rptData,报告的我的信息。

03.2.1 reportTemplate 代码

该类负责按照报告类型绘制各种报告组件。

因为 rptTitle、rptTail、rptPersonalInfo、rptResult几乎每一个报告都有,所以就按照固定方式配置在组件内。

<template>
  <div class="rptTemplate"> <vue-lazy-component :timeout="1000"> <rpt-title :data="rptData" /> <rpt-personal-info :data="rptData" /> <div v-for="(com,index) in config" :key="index"> <rpt-total-table v-if="totalTable(com)" :data="st" :config="com" /> <rpt-guage v-if="guage(com)" :data="st" :config="com" /> <rpt-single-line v-if="singleLine(com)" :data="st" :config="com" /> </div> <rpt-result :data="report" :config="rptData" /> <rpt-tail :data="rptData" /> </vue-lazy-component> </div>
</template>

<script> import rptTitle from '../rptTitle' import rptTail from '../rptTail' import rptResult from '../rptResult' import rptPersonalInfo from '../rptPersonalInfo' import rptTotalTable from '../rptTotalTable' import rptGuage from '../rptGuage' import rptSingleLine from '../rptSingleLine' export default { name: 'RptTemplate', components: { rptTitle, rptTail, rptResult, rptPersonalInfo, rptTotalTable, rptGuage, rptSingleLine }, props: { rptData: { type: Object, default: () => { return {} }, }, report: { type: Object, default: () => { return {} }, }, st: { type: Object, default: () => { return null }, }, config: { type: Array, default: () => { return [] }, }, }, data() { return { id: `${this.type}`, } }, computed: { }, created() { console.log('subcom:' + this.type) }, methods: { totalTable(config) { return this.getConfigValue(config, 'rptTotalTable') }, stackLine(config) { return this.getConfigValue(config, 'rptStackLine') }, guage(config) { return this.getConfigValue(config, 'rptGuage') }, singleLine(config) { return this.getConfigValue(config, 'rptSingleLine') }, getConfigValue(config, key) { if (config && 'type' in config && config.type == key) { return config } else { return null } }, }, } </script>

<style rel="stylesheet/scss" lang="scss" scoped> .rptTemplate{ width:100%; padding: 0 15px; } </style>
复制代码

03.2.2 rptTitle等组件 代码

按可复用的粒度,切分报告的各个部分为组件,突然发现组件实现超级简单了。

好比标题切分红组件后,只须要关心怎么显示标题、图片等。

<template>
  <div class="titleSpan"> <table class="printTable"> <tr v-if="logo && !data.hiddenTitle"> <td valign="top" align="center"> <img :src="logo" style="max-height: 100px" /> </td> </tr> <tr v-if="!data.hiddenTitle"> <td align="center"> <!-- margin-top: 60px; --> <div style="text-align: center; font-size: 38px; height: 60px"> {{ data.SYSTEM_NAME }} </div> </td> </tr> <tr> <td align="center"> <div :class="data.hiddenTitle ? 'Bigtitle' : 'title'"> {{ data.SCALE_NAME }}评估报告单 </div> </td> </tr> <tr> <td> <div style="text-align: right; font-size: 18px; "> <div style="line-height: auto"> {{ data.REPORT_ID }} </div> </div> </td> </tr> </table> </div>
</template>

<script> import { mapGetters } from "vuex"; export default { name: "RptTitle", props: { data: { type: Object, default: () => { return {}; } } }, data() { return {}; }, computed: { ...mapGetters(["sysConfig"]), styleObject() { return { color: this.$options.filters["statusColor3"](this.data.alertValue) }; }, logo() { return this.sysConfig && this.sysConfig["report.logo"] ? `/api/tools/download/${this.sysConfig["report.logo"]}` : ""; } }, }; </script>

<style rel="stylesheet/scss" lang="scss" scoped> .inline { display: inline; width: 15px; height: 15px; } .printTable { width: 100%; } .Bigtitle { text-align: center; font-size: 32px; height: 60px; margin-top: 50px; } .title { text-align: center; font-size: 28px; height: 40px; } </style>
复制代码

03.2.3 仪表盘组件 代码

image.png 仪表盘组件按照每行4个显示,而且为了打印美观,设定该组件总体换页page-break-inside: avoid;

根据须要,还能够设定配置属性,以便配置仪表盘的最大值,切分几块,分区颜色等。

<template>
  <div class="printBlock"> <div v-if="config.title" class="title"> {{ this.$t("report." + config.title) }} </div> <table style="width:100%;border:1px solid #000"> <tr v-for="(g, x) in chartData" :key="x"> <td v-for="(item, y) in g" :key="y" align="center"> <v-chart ref="line" class="chart" :theme="theme" :autoresize="true" :init-options="initOptions" :option="options[4 * x + y]" /> </td> </tr> </table> </div>
</template>

<script> export default { name: "RptGuage", props: { data: { type: Object, default: () => { return null; } }, config: { type: Object, default: () => { return {}; } } }, data() { return { initOptions: { renderer: "canvas", locale: this.$i18n.locale }, theme: "default", // default\light\dark option: { series: [ { type: "gauge", min: 0, max: 5, splitNumber: 5, axisLine: { lineStyle: { width: 15, color: [ [0.25, "#7CFFB2"], [0.5, "#0eb83a"], [0.75, "#FDDD60"], [1, "#FF6E76"] ] } }, pointer: { itemStyle: { color: "auto" } }, axisTick: { distance: -5, length: 10, lineStyle: { color: "#fff", width: 2 } }, splitLine: { distance: -10, length: 20, lineStyle: { color: "#fff", width: 4 } }, axisLabel: { color: "auto", distance: 10, fontSize: 14 }, detail: { valueAnimation: true, formatter: "{value}", // offsetCenter: [0, '0%'], color: "auto", fontSize: "16" }, title: { show: true, offsetCenter: [0, "95%"] }, data: [ { value: 70, name: "人际关系敏感" } ] } ] }, options: [], chartData: [] }; }, created() { this.chartData = []; this.options = []; const arr = this.config.formatData(this.data); for (let i = 0; i < arr.length; i += 4) { const len = Math.min(4, arr.length - i); if (arr.length < 4) len = arr.length; this.chartData.push(arr.slice(i, i + len)); for (let j = 0; j < len; j++) { const opt = JSON.parse(JSON.stringify(this.option)); if (this.config.scale) { if (this.config.scale.length > i + j) { opt.series[0].max = this.config.scale[i + j].max || 5; opt.series[0].splitNumber = this.config.scale[i + j].splitNumber || 5; opt.series[0].axisLine.lineStyle.color = this.config.scale[ i + j ].color; } else { opt.series[0].max = this.config.scale[0].max || 5; opt.series[0].splitNumber = this.config.scale[0].splitNumber || 5; opt.series[0].axisLine.lineStyle.color = this.config.scale[0].color; } } opt.series[0].data[0] = { title: { width: 160, overflow: "break" }, ...arr[i + j] }; this.options.push(opt); } } }, }; </script>

<style rel="stylesheet/scss" lang="scss" scoped> .chart { width: 160px; //100%打印有bug height: 160px; border: 0px solid #000; } .title { width: 100%; text-align: center; font-weight: 800; font-size: 22px; margin: 20px auto; } .printBlock { page-break-inside: avoid; } </style>

复制代码

03.2.4 折线图代码

注意: 由于data内没法使用计算属性跟踪变化,所以若是须要初始化数据后显示的化,应该在组件属性赋值前处理。

而我由于是后期才有相似需求,所以被逼在 created时初始化数据,并经过对echart的option属性修改,触发Echart的重绘,有点笨拙。

<template>
  <div> <div v-if="config.title" class="title"> {{ this.$t("report." + config.title) }} </div> <v-chart ref="line" class="chart" :theme="theme" :autoresize="true" :init-options="initOptions" :option="option" /> </div>
</template>

<script> export default { name: "RptSingleLine", props: { data: { // scoresTool type: Object, default: () => { return null; } }, config: { type: Object, default: () => { return {}; } } }, // 因线图 created() { if (this.config.init) { this.config.init(this.data); this.option.legend.data = this.config.keys; this.option.xAxis.data = this.config.keys; this.option.series[0].data = this.chartData(); } }, data() { return { initOptions: { renderer: "canvas", locale: this.$i18n.locale }, theme: "default", // default\light\dark option: { title: { text: "", show: true, subtext: "西安西京医院-by webmote", // textAlign:'center', left: "right", top: "-10" }, tooltip: { trigger: "axis" }, legend: { width: 580, data: this.config.keys }, grid: { left: "5%", right: "5%", bottom: "5", containLabel: true }, xAxis: { type: "category", boundaryGap: true, data: this.config.keys, axisTick: { interval: 0, alignWithLabel: true }, axisLabel: { interval: 0, rotate: this.config.keys.length > 6 ? 30 : 0 } }, yAxis: { name: this.$t("report." + this.config.yAxis), nameLocation: "middle", nameGap: 40, type: "value", min: 0, max: 100 }, series: [ { data: this.chartData(), type: "line", smooth: true } ] } }; }, methods: { chartData() { return this.config.formatData(this.data); } } }; </script>

<style rel="stylesheet/scss" lang="scss" scoped> .chart { width: 700px; //100%打印有bug height: 300px; border: 1px solid #000; } .title { width: 100%; text-align: center; font-weight: 800; font-size: 22px; margin: 20px auto; } </style>

复制代码

03.2.5 报告配置文件定义

配置不少了,这里展现了默认的报告配置。 st是来自报告的相关数据,为了绘制图和仪表盘,总须要相关数据的。

🐢🐢🐢按着个人龟速算,不包含组件编写的话,基本3个小时能够完成20-30个配置的编写。

这查看和编写拷贝,已经让我烦不胜烦了。

作完后,我后悔了。

哎,先作个报告编辑器就行了,又能够涨一波技能了。

export default {
  default: [    
    {
      type: 'rptGuage',
      title: 'factorImage',    
      formatData: function(st) {       
        const keys = st.getScoreCols()
        if (!st) return []
        const arr = []
        keys.forEach(name => {
          if (name) {
            const data = st.getRaw(name)
            arr.push({
              name: name,
              value: data,
            })
          }
        })

        return arr
      },
    },
    {
      type: 'rptFactorTable',
      title: '',
      cols: [
        {
          name: 'factor',
          width: '15%',
        },
        {
          name: 'scoreValue',
          width: '10%',
        },
        {
          name: 'reducingRate',
          width: '10%',
        },
        {
          name: '',
          width: '15%',
        },
        {
          name: 'factor',
          width: '15%',
        },
        {
          name: 'scoreValue',
          width: '10%',
        },
        {
          name: 'reducingRate',
          width: '10%',
        },
        {
          name: '',
          width: '15%',
        }],    
      formatData: function(st) {
        const keys = st.getScoreCols()
        if (!st) return []
        const arr = []
        for (let i = 0; i < keys.length; i += 2) {
          arr.push([
            keys[i], st.getRawString(keys[i]), st.getRawReducingRate(keys[i]), '',
            keys[i + 1], st.getRawString(keys[i + 1]), st.getRawReducingRate(keys[i + 1]), '',
          ])
        }
        return arr
      },
    },

    {
      type: 'rptStackLine',
      title: 'historyReducingRate',   
      formatData: function(st) {
        const keys = st.getScoreCols()       
        if (!st) return []
        const arr = []
        keys.forEach(name => {
          if (name) {
            const data = st.getAllRawReducingRate(name)
            arr.push({
              name: name,
              type: 'line',
              data: data,
            })
          }
        })

        return arr
      },
    },
  ],
  ... //能够增长各个报告类型的配置
  }
复制代码

🎏 04.再看看效果?

作完问卷调查,就是报告列表了。

咱们处的这个时代,内卷太厉害,不过无论你是抑郁仍是焦虑,本系统都能给你测一测。

image.png

查看报告~~

image.png

除了报告,本系统的算法也是很值钱的。

🎏 05.打印的缺陷——页眉页脚

利用脚本打印的报告总体是OK的,但页眉页脚显示出来比较难看,会显示网页连接等信息。

仅有2个方法能搞定它:

  • 利用打印选项,勾选掉页眉页脚选项,须要教导客户

image.png

  • 设置打印上下页边距为 3mm
// 去除页眉页脚
    @page {
            size: auto A4 landscape;
            margin: 3mm;
        }
        html{
    background-color: #FFFFFF;
    margin: 0;  /* this affects the margin on the html before sending to printer */
  }
 
  body{
    border: solid 1px blue ;
    margin: 10mm 15mm 10mm 15mm; 
    }
复制代码

注意: 不要考虑定制页眉页脚,仅仅经过js方案是搞不定的,无数大牛已经证实这一点,别再浪费时间了! (我浪费了不少时间在这上面...)

有定制页眉页脚硬需求的

  • 请在服务端生成pdf,而后打印。

  • 或者安装打印插件...这个我没用过。

🎏 06. 结语

报告前先后后搞了有1周? 由于上班期间大概持续了有大半个月吧,只算纯时间,估计有1周,最后总算顺利搞定了,惟一的遗憾就是没有报告设计器

先把功能搞定,这也是作项目的基本原则。

下一个版本再增长报告设计器!

年少不识前端香,🕺🕺🕺 错把后端当个宝!

例行小结,理性看待!

结的是啥啊,结的是我想你点赞而不可得的寂寞。😳😳😳

👓都看到这了,还在意点个赞吗?

👓都点赞了,还在意一个收藏吗?

👓都收藏了,还在意一个评论吗?

还有系列前端文章,客官,你不瞧瞧?

👉关于微前端(阿里QianKun)的那点事——上线一个“微前端”逼走了2位90后

👉前端项目,看我在这里管理全局后台初始化的数据,就问你飒不飒?

👉十分钟手把手教你设计简单易用的组件级考试题(单选、多选、填空、图片),建议收藏

👉解放前端工程师——手把手教你开发本身的自定义列表和自定义表单系列之一缘起

👉解放前端工程师——手把手教你开发本身的自定义列表和自定义表单系列之二接口

👉解放前端工程师——手把手教你开发本身的自定义列表和自定义表单系列之三表格

👉Vue组件定制——动态查询规则生成组件

相关文章
相关标签/搜索