「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!」javascript
- 📢歡迎點贊 :👍 收藏 ⭐留言 📝 若有錯誤敬請指正,賜人玫瑰,手留餘香!
- 📢本文做者:由webmote 原創,首發於 【掘金】
- 📢做者格言: 生活在於折騰,當你不折騰生活時,生活就開始折騰你,讓咱們一塊兒加油!💪💪💪
掘友們,你們好,我又來了。🕺🕺🕺css
你們在工做中最煩惱的是什麼? 是否是重複作相似的工做啊?你有過設計報告作到吐的感覺嗎?html
是的,最近我碰上了個大麻煩🥺,Ctrl +C、V鍵快被我敲掉了。前端
它就是製做價值XX w💰💰💰(聽說具體數字容易被舉報,這裏用XX替換)的某醫院用報告單。 該項目主要作心理問卷,而後根據問卷、經後臺算法後解析出報告。因爲處理的是各類類型的心理體檢報告單,因此花樣繁多,總共有100+ 的不一樣報告須要展現和打印。接手項目的時候,對這些報告單算是懵懂無知,看了幾個感受大同小異,就誤覺得差很少都相似。vue
還好報酬足夠豐厚,要否則對不起我這快要敲壞的手,看我二指禪✌️。java
通過叮叮噹噹一陣響的腳手架、環境的準備,我以我最快的速度搞定了基本數據的增刪改查工做(感謝 vue-element-admin項目),是時候表演製做報告的拿手絕活了。webpack
最最核心💕的也就是圖文混排報告了,先秀秀效果。git
💫第一方隊是問卷調查報告和艾森克個性測驗報告。github
💫第二方隊是明尼蘇達多相人格調查表評估報告。 web
💫第三方隊是多項人格調查表評估報告。 但願能給新手以啓迪💏,讓老手有東西吐槽💏。
💫第四方隊是...
打住!後面的方隊都回去吧,領導不審閱了,都擦球很少的樣子。
一篇圖文混排可打印報告單的技術實現,主要涉及到的技術是表格🏢、各種圖📊、📈、各種報告塊📃、打印🖨️。
輪子雖然也要造,但咱們選擇站在巨人的肩膀上造輪子,畢竟站得高看得遠,能省一點是一點。
下面列下使用的三方庫或包:
其核心思想是把打印的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;
},
複製代碼
「小夥子,來,姨給你社(說)句話...」
住過西安城中村(吉祥村)的娃都應該聽過這個段子。
如今活來了,⚡你攤上事了⚡。
需求: 製做報告,每種報告都須要處理不一樣的數據,展現不一樣的格式;
往下看以前,不妨留給本身5分鐘⏱️思考時間,看看咱們的實現有哪些差別?
金樽清酒鬥十千,玉盤珍羞直萬錢。🥂🥂🥂
停杯投箸不能食,拔劍四顧心茫然。🤺🤺🤺
欲渡黃河冰塞川,將登太行雪滿山。🚶♀️🚶♀️🚶
閒來垂釣碧溪上,忽復乘舟夢日邊。🎣🎣🎣
行路難,行路難,多歧路,今安在?🚶♂️🚶♂️🚶
長風破浪會有時,直掛雲帆濟滄海。🏄🏄🏄
所謂「動態模板方案」,就是按照報告類型定製該類型的模板組件。
咱們只須要判斷模板類型,而後加載相應模板進行渲染,就搞定了這個需求,是否是超簡單?
看下代碼組織形式:
<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>
複製代碼
這裏利用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>
複製代碼
報告模板的內容較多,這裏會簡化一部分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>
複製代碼
使用動態模板報告組件,就很容易了。
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>
複製代碼
100個模板我已經Ctrl+C、V完了,命名也都改了一遍。
只等按照報告類型,逐一修改每一個模板的html定義,以及渲染顯示實現了。
天,還有渲染顯示的邏輯呢!!!✨這,真的要把手敲斷啊?✨
每一個報告有一部分是類似的,好比我的資料,簽名提示等,這些就算都作成組件,我也得100個模板一個個複製過去啊!
😂我已經哭暈在廁所了😂,錢真尼瑪很差掙~~ 我退出好很差?
我感受本身已經上了梁山,下不來了。
而且我感受打包速度有點慢,利用 webpack-bundle-analyzer
插件掃描了下代碼,templates模板文件夾所佔性能比重超大! 100個模板組件不是蓋的~~
報告類型太多了,必須換方案,要不這重複的報告拷貝來拷貝去煩都煩死了。🥺
喝杯白開水,🧺閉目養神10分鐘。
好了,冷靜事後,加油, webmote!
重要的時刻須要冷靜下來,而後再開動腦筋。
先繪製下圖。
抽象一下: 每一個報告都由不一樣的組件按照順序結構排列而成。
順序結構能夠看數組
,不一樣的組件可能會有不一樣的屬性定義,那麼若是使用配置來定義一個報告,能夠定義以下結構:
't0-000': [{},{},{}],
't0-001': [{},{},{}],
't0-002': [{},{},{}],
...
複製代碼
先看看能不能解決方案1的問題🔥?
若是t0-100
的報告格式和t0-002
的報告格式類似,則能夠複製配置,看起來這個工做量是可控的。
那{}
,組件的屬性是什麼鬼東西呢?
嗯,咱們暫且不要抽象,用到一個具體組件時在定義不遲。
既然已經由了初步的構思,那讓咱們先實現默認報告配置吧!
報告使用代碼:
<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,報告的我的信息。
該類負責按照報告類型繪製各種報告組件。
因爲 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>
複製代碼
按可複用的粒度,切分報告的各個部分爲組件,突然發現組件實現超級簡單了。
好比標題切分紅組件後,只須要關心怎麼顯示標題、圖片等。
<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>
複製代碼
儀表盤組件按照每行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>
複製代碼
注意: 由於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>
複製代碼
配置不少了,這裏展現了默認的報告配置。 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
},
},
],
... //能夠增長各個報告類型的配置
}
複製代碼
作完問卷調查,就是報告列表了。
咱們處的這個時代,內卷太厲害,不過無論你是抑鬱仍是焦慮,本系統都能給你測一測。
查看報告~~
除了報告,本系統的算法也是很值錢的。
利用腳本打印的報告總體是OK的,但頁眉頁腳顯示出來比較難看,會顯示網頁連接等信息。
僅有2個方法能搞定它:
// 去除頁眉頁腳
@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,而後打印。
或者安裝打印插件...這個我沒用過。
報告前先後後搞了有1周? 由於上班期間大概持續了有大半個月吧,只算純時間,估計有1周,最後總算順利搞定了,惟一的遺憾就是沒有報告設計器。
先把功能搞定,這也是作項目的基本原則。
下一個版本再增長報告設計器!
年少不識前端香,🕺🕺🕺 錯把後端當個寶!
例行小結,理性看待!
結的是啥啊,結的是我想你點贊而不可得的寂寞。😳😳😳
👓都看到這了,還在意點個贊嗎?
👓都點讚了,還在意一個收藏嗎?
👓都收藏了,還在意一個評論嗎?
還有系列前端文章,客官,你不瞧瞧?
👉關於微前端(阿里QianKun)的那點事——上線一個「微前端」逼走了2位90後
👉前端項目,看我在這裏管理全局後臺初始化的數據,就問你颯不颯?
👉十分鐘手把手教你設計簡單易用的組件級考試題(單選、多選、填空、圖片),建議收藏
👉解放前端工程師——手把手教你開發本身的自定義列表和自定義表單系列之一緣起
👉解放前端工程師——手把手教你開發本身的自定義列表和自定義表單系列之二接口