VUE 表格元件進階-排序

身為一個前端工程師常常會遇到處理表格的問題,SA 開出來的需求大概就是排序、一頁顯示幾筆資料及分頁等功能。這裡我先筆記一下,如何達成排序的功能。

基本上我們會希望表格會是一個元件,由父元件定義好表頭欄位及取得API傳來的資料透過 prop 的方式傳入子元件,以達成表格元件重複使用的目的。

建立表格結構

首先建立表格元件,預設 fields 跟 data 是由父元件透過 prop 傳入表格子元件的資料。

<script setup>
import { ref } from 'vue';
const props = defineProps(['fields', 'data']);
</script>

而父層的 fields 跟 data 長這個樣子:

const fields = ref([
  {
    apikey: 'team',
    label: '小隊',
    sortable: false,
    width: 60,
  },
  {
    apikey: 'name',
    label: '姓名',
    sortable: true,
    width: 110,
  },
  {
    apikey: 'gang',
    label: '幫派',
    sortable: true,
    width: 110,
  },
  {
    apikey: 'teacher',
    label: '師承',
    sortable: true,
    width: 150,
  },
]);

const data = ref([
  {
    team: '青龍',
    name: '郭靖',
    teacher: '洪七公',
    gang: '全真教',
  },
  {
    team: '白虎',
    name: '黃蓉',
    teacher: '黃藥師',
    gang: '桃花島',
  },
  {
    team: '玄武',
    name: '楊過',
    teacher: '小龍女',
    gang: '古墓派',
  },
]);

然後利用 fields 跑 v-for 建立表頭:

 <thead>
      <tr>
        <th v-for="(field, index) in props.fields" :key="field.apikey">
          <div class="d-flex flex-row align-items-center">
            <span>{{ field.label }}</span>
            <div>
              <i class="bi bi-caret-up-fill"></i>
              <i class="bi bi-caret-down-fill"></i>
            </div>
          </div>
        </th>
      </tr>
 </thead>

表格的主體部分,<tr>的部分是利用 data 跑 v-for以列出每一筆資料,而 <td> 的部分則是對應 fields 來跑 v-for,這裡比較特別的是要用 fields 的 apikey 來取 data 對應的 key 的 value。

 <tbody>
      <tr v-for="(item, index) in data" :key="item.name">
        <td scope="row" v-for="(obj, tdIndex) in fields" :key="obj.name">
          {{ item[obj.apikey] }}
        </td>
      </tr>
    </tbody>

以上就是表格的基本結構,範例原始碼

建立資料排序功能

如果你有注意到,表頭的每一欄我都有放上 2 個箭頭,一個朝上,一個朝下,我希望點擊朝上箭頭時可以依據那一欄的資料進行升冪排序,而點擊向下箭頭時就是比較該欄資料進行降冪排序。

在這之前需要先理解 number()sort() 這兩個知識點:

使用 array.sort() 的時候,如果沒有提供比較函式,是依據 Unicode 字典順序排序,排出來的結果可能不如預期,所以關於排序,我們第一個要做的事情就是提供比較(compare)函式:

先判斷傳入比較的資料是否為 number ,如果是數字則用比大小的方式排序,否則使用 JavaScript 提供的 localeCompare 方法進行比較。

另外也要考慮到一種狀況,也就是比較的資料其中之一為數字,另一個為字串時,確保數字排在前面。

const compareValues = (value1, value2) => {
  let valueA = Number(value1);
  let valueB = Number(value2);

  if (!isNaN(valueA) && !isNaN(valueB)) {
    // 兩者皆為數字,進行數值比較
    return valueA - valueB;
  } else if (isNaN(valueA) && isNaN(valueB)) {
    // 兩者皆為字串,使用 localeCompare 比較
    return String(value1).localeCompare(String(value2));
  } else {
    // 其中一個是數字,確保數字排在前面
    return isNaN(valueA) ? 1 : -1;
  }
};

然後我們用  isAscend 這個變數來控制升冪還是降冪排序,預設是升冪排序。

const isAscend = ref(true);

接下來利用比較函式 compareValues 來建立排序函式:

const sortValue = (key) => {
  isAscend.value = !isAscend.value;
  const sortedArray = [...tempData.value];
  if (isAscend.value) {
    sortedArray.sort((a, b) => compareValues(a[key], b[key]));
  } else {
    // 如果 false 反轉
    sortedArray.sort((a, b) => compareValues(b[key], a[key]));
  }
  tempData.value = sortedArray;
};

利用 isAscend.value 來做升降冪切換與判斷:

isAscend.value = !isAscend.value;

其中值得注意是,因為單向資料流的關係,我們不對傳入子元件的資料進行處理,所以把 props.data 賦值給 tempData 這個 ref 變數 :

const tempData = ref(props.data);

而且在 sortValue 這個函式裡,我們也不直接改變 tempData.value,而是解構後賦值給 sortedArray 這個常數,再來進行排序。

然後在兩個上、下箭頭上綁定 sortValue 這個函式:

<div>
      <i
         class="bi bi-caret-up-fill"
        :class="[isAscend ? 'true-class' : 'false-class']"
        @click="sortValue(field.apikey)"
      ></i>
      <i
         class="bi bi-caret-down-fill"
        :class="[isAscend ? 'false-class' : 'true-class']"
        @click="sortValue(field.apikey)"
      ></i>
</div>

這樣就完成排序的功能了,詳見範例程式碼