打印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組件定製——動態查詢規則生成組件

相關文章
相關標籤/搜索