基于Quasar实现一个树状表格TableTree


Quasar 是一个基于 Vue3 的前端UI框架,使用的是 Material Design, 对于国内而言(包括我)觉得确实不太好看,但因为之前使用的是 Element-UI,大概是由于审美疲劳,此次升级我的后台管理UI,没有继续使用 Vue3 版本的 Element-Plus。 不久前我也是基于 Quasar 开发了一个 Memos 客户端, 感觉效果还不错

前言

Quasar 默认没有树状表格的支持,但我觉得树状表格其实还挺重要的,尤其是在显示一些树状的数据,比如一个部门列表(打比方), 树状表格能够清晰的表明各部门之间的上下关系,目前我看实现树状表格的示例要么是基于 qhierarchy (使用 q-markup-table 实现,定制太困难), 要么就是实现方式太复杂 tree-table-example, 所以我研究了一下,发现其实还挺简单的,故此记录

实现一(废弃)

虽然 Quasar 没有树状表格的组件, 但 q-table 默认是支持 Expanding rows 的,所以我一开始的想法是使用这个功能实现一个递归的组件, 就像这样

 1<template>
 2  <q-tr :key="row.id" :props="props">
 3    <slot :row="row" :status="status" :indent="indent" :indentStyle="indentStyle(indent)"></slot>
 4  </q-tr>
 5  <template :key="index" v-for="(child, index) in row.children" v-if="status.expand">
 6    <table-tree :props="props" :indent="indent + 1" :row="child" v-slot="scope">
 7      <slot :row="scope.row" :status="scope.status" :indent="scope.indent" :indentStyle="indentStyle(scope.indent)"></slot>
 8    </table-tree>
 9  </template>
10</template>

而在父组件中调用只需要把原来的 props 改成 scope

 1<template v-slot:body="props">
 2  <table-tree :props="props" :row="props.row" v-slot="scope">
 3    <q-td auto-width>
 4      <q-checkbox size="sm" v-model="scope.status.selected" />
 5    </q-td>
 6    <q-td>
 7      <q-btn flat round dense size="xs"
 8             :icon="scope.status.expand ? 'remove' : 'add'"
 9             @click="scope.status.expand = !scope.status.expand"
10             v-if="scope.row.children && scope.row.children.length > 0" />
11      {{ scope.row.name }}
12    </q-td>
13    <q-td>{{ scope.row.desc }}</q-td>
14  </table-tree>
15</template>

但是后面发现表格在多选状态下无法和原有的表格选择进行联动,只能自己实现选中和取消选中的方法,虽然也能实现,当很麻烦,只好另寻其它方式

实现二

后面研究了一下 Fernando2684 的实现方式, 虽然他的方式同样复杂,当实现原理其实很简单, 那就是修改原始数据,比如第二行需要进行展开,那我就在原来第二行的数据下把第二行的子数据追加上去,取消展开就把子数据删除,恢复原来的数据。有了这个原理后就好办了,我们可以利用 Expanding rows 时用到的 props.expand 参数来控制是否展开子数据, 然后利用计算属性动态的计算展开后的数据

  • 表格模版

     1<q-table flat bordered
     2         row-key="id"
     3         separator="cell"
     4         selection="multiple"
     5         - :rows="table.list"
     6         + :rows="expandRows"
     7         v-model:expanded="table.expanded"
     8         v-model:selected="table.selected"
     9         v-model:pagination="table.pagination">
    10  <template v-slot:body="props">
    11    <q-tr :props="props">
    12      <q-td auto-width>
    13        <q-checkbox size="sm" v-model="props.selected" />
    14      </q-td>
    15      <q-td>
    16        <q-btn flat round dense size="xs"
    17               :icon="props.expand ? 'remove' : 'add'"
    18               @click="props.expand = !props.expand"
    19               v-if="props.row.children && props.row.children.length > 0" />
    20        {{ props.row.name }}
    21      </q-td>
    22      <q-td>{{ props.row.desc }}</q-td>
    23    </q-tr>
    24  </template>
    25</q-table>
  • 表格数据

     1const table = ref({
     2    list: [],
     3    expanded: [],
     4    selected: [],
     5    loading: true,
     6    pagination: {
     7        page: 1,
     8        sortBy: 'desc',
     9        descending: false,
    10        rowsPerPage: 0,
    11        rowsNumber: 0,
    12    }
    13})
    14
    15const expandRows = computed(() => {
    16    return makeRows(table.value.list, table.value.expanded)
    17})

    这里提一嘴,pagination.rowsPerPage 默认最好设置为0(不限制每页的数量, 但会由由后端控制返回的行数), 否则展开子数据后的数据会被放到第二页, 影响数据查看

  • 动态计算展开后的数据

     1function makeRows(rows, expanded) {
     2    if (!rows) {
     3        return []
     4    }
     5    if (expanded.length == 0) {
     6        return rows
     7    }
     8    let newRows = []
     9    rows.forEach(row => {
    10        newRows.push(row)
    11        if (expanded.indexOf(row.id) > -1) {
    12            newRows = newRows.concat(makeRows(row.children, expanded))
    13        }
    14    })
    15    return newRows
    16}

    注意,这里因为要递归计算多级子目录,所以不能直接在 setup 上直接用 const 定义

  • 缩进:目前缩进确实有些问题,其中一个解决办法是原始数据会带有 parent_id 字段,可以通过判断 parent_id 大于0时增加缩进

    缩进的计算可以通过makeRows函数在遍历时添加每行数据的层级

     1function makeRows(rows, expanded, indent) {
     2    if (!rows) {
     3        return []
     4    }
     5    let newRows = []
     6    rows.forEach(row => {
     7        row._indent = indent
     8        newRows.push(row)
     9        if (expanded.indexOf(row.id) > -1) {
    10            newRows = newRows.concat(makeRows(row.children, expanded, indent + 1))
    11        }
    12    })
    13    return newRows
    14}
    15
    16export default function useTable(table) {
    17    const expandRows = computed(() => {
    18        return makeRows(table.value.list, table.value.expanded, 0)
    19    })
    20    return {
    21        expandRows,
    22    }
    23}

    然后在表格中添加对应的偏移即可, 需要注意的是表格对齐方向需要设置为 left (默认值)

    1<template v-slot:body-cell-name="props">
    2  <q-td :props="props" :style="'padding-left:' + ((props.row._indent || 0) + 1) + 'rem'">
    3    <q-btn flat round dense size="xs"
    4           :icon="props.expand ? 'remove' : 'add'"
    5           @click="props.expand = !props.expand"
    6           v-if="props.row.children && props.row.children.length > 0" />
    7    {{ props.row.name }}
    8  </q-td>
    9</template>

最终实现

作者: honmaple
链接: https://honmaple.me/articles/2023/04/基于Quasar实现一个树状表格TableTree.html
版权: CC BY-NC-SA 4.0 知识共享署名-非商业性使用-相同方式共享4.0国际许可协议
wechat
alipay

加载评论