使用 vue3 + el-table 實現(xiàn)行列轉(zhuǎn)換 —— 成績單

行列轉(zhuǎn)換

為啥會出現(xiàn)行列轉(zhuǎn)換的問題呢戚啥?因為用戶想看的是一張多列的大表單,但是數(shù)據(jù)庫里面保存的是“單列”的數(shù)據(jù)锉试。于是就需要做一個轉(zhuǎn)換猫十,這樣客戶看得更方便清晰。

SQL有一種寫法可以支持這種行列轉(zhuǎn)換,但是寫起來比如繞炫彩,不便于理解匾七。
所以我個人還是傾向于在前端實現(xiàn)轉(zhuǎn)換的操作,因為可以節(jié)省后端的性能資源江兢。

這里以成績單為例演示一下具體的實現(xiàn)方式昨忆。

分析成績單的組成

這個又愛又恨的東東想必大家都不會陌生,這是一個比較典型的需要行列轉(zhuǎn)換的情景杉允。
先回顧一下成績單的樣子:(圖片來自于網(wǎng)絡(luò)邑贴,侵刪)


成績單

分析一下,可以得到如下幾個基本元素:

  • 學(xué)科:語文叔磷、數(shù)學(xué)拢驾、物理、化學(xué)等改基。確定列數(shù)
  • 學(xué)生:確定行數(shù)
  • 年級班級:一年一班繁疤、一年二班等。分類依據(jù)
  • 考試:期中考試秕狰、期末考試等稠腊。分類依據(jù)
  • 成績:確定數(shù)據(jù)內(nèi)容。

元素的分類:

  • 分類依據(jù):指的是生成一個大表單的查詢條件鸣哀,同類別的數(shù)據(jù)匯總成一個大表單架忌。
  • 學(xué)科:確定列數(shù),科目越多列數(shù)也就越多我衬。
  • 學(xué)生:確定行數(shù)叹放,學(xué)生越多行數(shù)也就越多。

我們再看看數(shù)據(jù)庫里的設(shè)計挠羔,一般會設(shè)計幾個基礎(chǔ)表和一個成績表井仰。

  • 學(xué)科表
  • 學(xué)生表
  • 班級表
  • 考試表
  • 成績表

篇幅有限,具體字段就不介紹了褥赊,說全的話也是挺復(fù)雜的糕档。

用 vue3 + el-table 做的成績單

行列轉(zhuǎn)換后的成績單

學(xué)科、學(xué)生拌喉、總分、平均分俐银、最高分尿背、最低分、名次都有了捶惜。還可以各種排序田藐。下面我們來看看是如何實現(xiàn)的。

前端模擬數(shù)據(jù)

我們在前端模擬一下數(shù)據(jù)。(簡化模式)

// 學(xué)科 —— 確定列
const subject = [
  { id: 1, name: '數(shù)學(xué)' },
  { id: 2, name: '語文' },
  { id: 3, name: '物理' },
  { id: 4, name: '化學(xué)' }
]

// 學(xué)生 —— 確定行
const student = [
  { id: 1, name: '張三' },
  { id: 2, name: '李四' },
  { id: 3, name: '王五' },
  { id: 4, name: '趙六' }
]

// 班級 —— 分類依據(jù)
const classes = [
  { id: 1, name: '一年一班' },
  { id: 2, name: '一年二班' }
]

// 考試 —— 分類依據(jù)
const exam = [
  { id: 1, name: '期中考試' },
  { id: 2, name: '期末考試' }
]

// 成績 —— 確定內(nèi)容
const reportCard = [
  // 序號   考試ID      班級ID       學(xué)生ID         科目ID        成績
  { id: 1, examId: 1, classId: 1, studentId: 1, subjectId: 1, score: 100 },
  { id: 2, examId: 1, classId: 1, studentId: 1, subjectId: 2, score: 98 },
  { id: 3, examId: 1, classId: 1, studentId: 1, subjectId: 3, score: 90 },
  { id: 4, examId: 1, classId: 1, studentId: 2, subjectId: 1, score: 90 },
  { id: 5, examId: 1, classId: 1, studentId: 2, subjectId: 2, score: 90 },
  { id: 6, examId: 1, classId: 1, studentId: 2, subjectId: 3, score: 40 },
  { id: 7, examId: 1, classId: 1, studentId: 3, subjectId: 1, score: 30 },
  { id: 8, examId: 1, classId: 1, studentId: 3, subjectId: 2, score: 90 },
  { id: 8, examId: 1, classId: 1, studentId: 3, subjectId: 3, score: 40 },
  { id: 9, examId: 1, classId: 1, studentId: 4, subjectId: 1, score: 64 },
  { id: 8, examId: 1, classId: 1, studentId: 4, subjectId: 2, score: 90 },
  { id: 9, examId: 1, classId: 1, studentId: 4, subjectId: 3, score: 70 }
]
 

使用 el-table 生成成績單

element-plus 提供了一個很強大的 表格組件 —— el-table汽久,可以實現(xiàn)很多基礎(chǔ)功能鹤竭,比如排序、調(diào)整寬度景醇、設(shè)置顏色臀稚、篩選等功能。那么我們可以使用這個組件來實現(xiàn)成績單三痰。

確定表頭

行列轉(zhuǎn)換的一大特點是吧寺,表頭(有多少列)并不是固定的,而是需要動態(tài)生成的散劫。
以成績單為例稚机,表頭列數(shù)由學(xué)科決定,學(xué)科越多获搏,表頭也就越多赖条。

首先我們按照 el-table 的要求設(shè)置一下表頭:

/**
 * 根據(jù)學(xué)科建立表頭
 * * 學(xué)號、姓名常熙、【各個學(xué)科】纬乍、總分、平均分症概、名次
 */
const createTableHead = () => {
  // 添加學(xué)生
  const head = [
  {
    prop: 'id',
    label: '學(xué)號',
    width: 120
  },
  {
    prop: 'name',
    label: '姓名',
    width: 120
  }]

  // 添加科目
  for (const key in subject) {
    const sub = subject[key]
    head.push({
      prop: 'sub_' + sub.id,
      label: sub.name,
      width: 120
    })
  }

  head.push({
    prop: 'totalScore',
    label: '總分',
    width: 120
  })
  head.push({
    prop: 'averageScore',
    label: '平均分',
    width: 120
  })
  head.push({
    prop: 'ranking',
    label: '名次',
    width: 120
  })
  
  return head
}

這里表頭分為兩種蕾额,一個是固定的,一個是根據(jù)科目動態(tài)生成的彼城。
我們采用簡單粗暴的方式诅蝶,先直接添加固定列,然后遍歷學(xué)科添加動態(tài)列募壕。

確定數(shù)據(jù)

表頭確定好了之后调炬,我們需要確定一下 data 部分,正式開始行列轉(zhuǎn)換舱馅。
還是按照 el-table 的需要來定義一下數(shù)據(jù)格式:

{
  id: 1,
  name: '張三',
  sub_1: 100,
  sub_2: 100,
  ...
  totalScore: 200,
  averageScore: 100,
  ranking: 1
}

中間可以有各個學(xué)科缰泡,屬性命名規(guī)則: 前綴 “sub_” + 學(xué)科ID 。
這樣便于后續(xù)添加數(shù)據(jù)代嗤。

遍歷成績表來填充數(shù)據(jù)棘钞。

/**
 * 行列轉(zhuǎn)換
 */
const rowToCol = () => {
  // 對象形式的成績列表
  const _code = {}
  // 數(shù)組形式的成績列表
  const _arr = []

  // 遍歷成績單
  for (const key in reportCard) {
    const rep = reportCard[key]
    if(typeof _code[rep.studentId] === 'undefined') {
      // 沒有記錄。創(chuàng)建一行學(xué)生成績干毅,加入固定列的數(shù)據(jù)
      _code[rep.studentId] = {
        id: rep.studentId, // 學(xué)生ID
        name: (student.filter((item)=>item.id === rep.studentId)[0] || {name:''}).name, // 根據(jù)id獲取學(xué)生姓名
        totalScore: 0, // 學(xué)生的各科總分悼沈,后續(xù)修改
        averageScore: 0, // 學(xué)生各科的平均分侣夷,后續(xù)修改
        ranking: 1 // 名次瑰步,后續(xù)修改
      }
    }
    // 記錄各科分數(shù)
    _code[rep.studentId]['sub_' + rep.subjectId] = rep.score
  }

  // 計算總分润歉、平均分
  下面介紹

  // 計算名次
  下面介紹

  return _arr
}

遍歷成績單的數(shù)據(jù)绅喉,先依據(jù)學(xué)生ID建立一個對象,然后根據(jù)學(xué)科確定各科成績叫乌。

計算學(xué)生的總分和平均分

一般成績單還需要得到學(xué)生的總分和平均分柴罐,我們可以遍歷成績統(tǒng)計總分和平均分。

  ...
  // 計算總分憨奸、平均分
  for(const key in _code) {
    const code = _code[key]
    // 遍歷學(xué)科
    let total = 0
    let ave = 0
    let count = 0
    for (const key in subject) {
      const fenshu = code['sub_' + subject[key].id]
      if (typeof fenshu !== 'undefined') {
        // 有成績革屠,計算總分和平均分
        count++
        total += fenshu
        ave = Math.floor(total / count * 10) / 10 // 保留一位小數(shù)。
        // 如果保留兩位小數(shù)的話可以這樣 (total / count ).toFixed(2)
      }
    }
    code.totalScore = total
    code.averageScore = ave
    // 對象的數(shù)據(jù)轉(zhuǎn)為數(shù)組的數(shù)據(jù)
    _arr.push(code)
  }

計算排名

成績單有了膀藐,我們還需要做一個排名屠阻。
如果不需要排名次的話,可以跳過此步驟额各。

  let _ranking = 0
  _arr.sort((a, b) => {
    // 按照總分倒序
    return b.totalScore - a.totalScore
  }).forEach((item) => {
    // 設(shè)置名次
    _ranking++
    _arr.find((a) => a.id === item.id).ranking = _ranking
  })

計算各個學(xué)科的平均分国觉。

這個也是比較常見的需求。
el-table 提供了自動求和的功能虾啦,同時也提供了自定義求和的方法麻诀,我們可以利用他來實現(xiàn)學(xué)科的平均分。

下面是官網(wǎng)給出的方法傲醉,本來是求和的蝇闭,稍微修改一下就可以得到學(xué)科的平均分。

 // 計算各學(xué)科的平均分
let i = 0
const  getSummaries = (param) => {
  i++
  if (i < 7) return []

  const { columns, data } = param;
  const sums = [];
  
  columns.forEach((column, index) => {
    if (index === 0) {
      sums[index] = '平均分'
      return
    }
    const values = data.map(item => Number(item[column.property]));
    if (!values.every(value => isNaN(value))) {
      sums[index] = values.reduce((prev, curr) => {
        const value = Number(curr)
        if (!isNaN(value)) {
          return prev + curr
        } else {
          return prev
        }
      }, 0);
      sums[index] =  Math.floor(sums[index] / values.length * 10) / 10
    } else {
      sums[index] = 'N/A';
    }
  })
  
  return sums

跟蹤了一下硬毕,發(fā)現(xiàn)這個函數(shù)會被調(diào)用七次呻引,這個次數(shù)似乎和行數(shù)、列數(shù)沒有關(guān)系吐咳。
而且前幾次的 return 沒啥作用逻悠,最后一次才會有效,所以加了一個判斷韭脊。

記錄各個科目的最高分和最低分

一張成績單還可以計算各種數(shù)據(jù)童谒,比如看看學(xué)科的最高分數(shù)、最低分數(shù)沪羔。
一般也可以關(guān)注一下這些統(tǒng)計數(shù)據(jù)饥伊,同樣我么可以用自定義求和的方法來實現(xiàn)需求,我么改進一下 getSummaries蔫饰。

// 計算各科目的平均分琅豆、最高分和最低分
const getSummaries = ({ columns }) => {
  i++
  if (i < 7) return []

  const sums = [];
   
  columns.forEach((item, index) => {
    if (item.property.indexOf('sub_') >= 0 ){
      const subjectId = item.property.replace('sub_', '')*1
      // 學(xué)科,計算平均值
      let ave = 0
      let sum = 0
      let max = 0
      let min = 99999
      const _arr = reportCard.filter((r) => r.subjectId === subjectId)
      _arr.forEach((item, index) => {
        sum += item.score // 求和
        if (max < item.score) max = item.score // 記錄最高分
        if (min > item.score) min = item.score // 記錄最低分
      })
      if (_arr.length === 0) {
        sums[index] = '-' // 沒有成績
      } else {
        // 計算平均分
        ave = Math.floor(sum/_arr.length * 10) / 10
        sums[index] = `${ave}(${max}-${min})`
      }
    } else {
      // 不計算
      sums[index] = ''
    }
  })
  sums[0] = '統(tǒng)計'
  return sums

}

這次我么直接同 reportCard 來統(tǒng)計平均分篓吁、最高分和最低分趋距。
同理,我么還可以統(tǒng)計一下及格人數(shù)越除、各個分段的人數(shù)。

增加排序功能

這個是 el-table 自帶的功能,我們加上就好摘盆。

<el-table
    :data="tableData"
    style="width: 100%;height: 300px;"
    :default-sort = "{prop: 'totalScore', order: 'descending'}"
    :row-class-name="tableRowClassName"
    border
    show-summary
    :summary-method="getSummaries"
  >
    <el-table-column
      v-for="(item, index) in tableHead"
      :key="'s'+ index"
      fixed
      sortable
      :prop="item.prop"
      :label="item.label"
      :width="item.width">
    </el-table-column>
  </el-table>

el-table 的屬性設(shè)置翼雀。default-sort 默認按照 總分倒序顯示。

增加色彩區(qū)分

如果想要把平均分低于60的著重表示出來怎么辦孩擂?el-table 也提供了功能狼渊,我們做一下判斷設(shè)置css即可。

// 顏色
const tableRowClassName = ({row, rowIndex}) => {
  if (row.averageScore < 60) { // 平均分不及格的同學(xué)
    return 'warning-row';
  } else if (row.averageScore > 95) { // 平均分超過95的同學(xué)
    return 'success-row';
  }
  return '';
}

源碼

https://gitee.com/naturefw/nf-vite2-element

小結(jié)

優(yōu)點:

  • 不需要后端做行列轉(zhuǎn)換类垦,只需要提供基礎(chǔ)數(shù)據(jù)即可狈邑,節(jié)省后端性能資源。
  • 功能比較全面蚤认,我能想到的基本都加上了米苹。

缺點:

  • 沒能做成通用的形式,其他行列轉(zhuǎn)換的需求還需要編寫代碼砰琢。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蘸嘶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子陪汽,更是在濱河造成了極大的恐慌训唱,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挚冤,死亡現(xiàn)場離奇詭異况增,居然都是意外死亡,警方通過查閱死者的電腦和手機训挡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門澳骤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人舍哄,你說我怎么就攤上這事宴凉。” “怎么了表悬?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵弥锄,是天一觀的道長。 經(jīng)常有香客問我蟆沫,道長籽暇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任饭庞,我火速辦了婚禮戒悠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘舟山。我一直安慰自己绸狐,他們只是感情好卤恳,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著寒矿,像睡著了一般突琳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上符相,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天拆融,我揣著相機與錄音,去河邊找鬼啊终。 笑死镜豹,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的蓝牲。 我是一名探鬼主播趟脂,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼搞旭!你這毒婦竟也來了散怖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤肄渗,失蹤者是張志新(化名)和其女友劉穎镇眷,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體翎嫡,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡欠动,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了惑申。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片具伍。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖圈驼,靈堂內(nèi)的尸體忽然破棺而出人芽,到底是詐尸還是另有隱情,我是刑警寧澤绩脆,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布萤厅,位于F島的核電站,受9級特大地震影響靴迫,放射性物質(zhì)發(fā)生泄漏惕味。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一玉锌、第九天 我趴在偏房一處隱蔽的房頂上張望名挥。 院中可真熱鬧主守,春花似錦榄融、人聲如沸剃袍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽业扒。三九已至,卻和暖如春蹭沛,著一層夾襖步出監(jiān)牢的瞬間章鲤,已是汗流浹背败徊。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工皱蹦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人沪哺。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓辜妓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親嫌拣。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容