基于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 的,所以我一开始的想法是使用这个功能实现一个递归的组件, 就像这样

<template>
  <q-tr :key="row.id" :props="props">
    <slot :row="row" :status="status" :indent="indent" :indentStyle="indentStyle(indent)"></slot>
  </q-tr>
  <template :key="index" v-for="(child, index) in row.children" v-if="status.expand">
    <table-tree :props="props" :indent="indent + 1" :row="child" v-slot="scope">
      <slot :row="scope.row" :status="scope.status" :indent="scope.indent" :indentStyle="indentStyle(scope.indent)"></slot>
    </table-tree>
  </template>
</template>

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

<template v-slot:body="props">
  <table-tree :props="props" :row="props.row" v-slot="scope">
    <q-td auto-width>
      <q-checkbox size="sm" v-model="scope.status.selected" />
    </q-td>
    <q-td>
      <q-btn flat round dense size="xs"
             :icon="scope.status.expand ? 'remove' : 'add'"
             @click="scope.status.expand = !scope.status.expand"
             v-if="scope.row.children && scope.row.children.length > 0" />
      {{ scope.row.name }}
    </q-td>
    <q-td>{{ scope.row.desc }}</q-td>
  </table-tree>
</template>

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

实现二

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

  • 表格模版

    <q-table flat bordered
             row-key="id"
             separator="cell"
             selection="multiple"
             - :rows="table.list"
             + :rows="expandRows"
             v-model:expanded="table.expanded"
             v-model:selected="table.selected"
             v-model:pagination="table.pagination">
      <template v-slot:body="props">
        <q-tr :props="props">
          <q-td auto-width>
            <q-checkbox size="sm" v-model="props.selected" />
          </q-td>
          <q-td>
            <q-btn flat round dense size="xs"
                   :icon="props.expand ? 'remove' : 'add'"
                   @click="props.expand = !props.expand"
                   v-if="props.row.children && props.row.children.length > 0" />
            {{ props.row.name }}
          </q-td>
          <q-td>{{ props.row.desc }}</q-td>
        </q-tr>
      </template>
    </q-table>
  • 表格数据

    const table = ref({
        list: [],
        expanded: [],
        selected: [],
        loading: true,
        pagination: {
            page: 1,
            sortBy: 'desc',
            descending: false,
            rowsPerPage: 0,
            rowsNumber: 0,
        }
    })
    
    const expandRows = computed(() => {
        return makeRows(table.value.list, table.value.expanded)
    })
    

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

  • 动态计算展开后的数据

    function makeRows(rows, expanded) {
        if (!rows) {
            return []
        }
        if (expanded.length == 0) {
            return rows
        }
        let newRows = []
        rows.forEach(row => {
            newRows.push(row)
            if (expanded.indexOf(row.id) > -1) {
                newRows = newRows.concat(makeRows(row.children, expanded))
            }
        })
        return newRows
    }
    

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

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

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

    function makeRows(rows, expanded, indent) {
        if (!rows) {
            return []
        }
        let newRows = []
        rows.forEach(row => {
            row._indent = indent
            newRows.push(row)
            if (expanded.indexOf(row.id) > -1) {
                newRows = newRows.concat(makeRows(row.children, expanded, indent + 1))
            }
        })
        return newRows
    }
    
    export default function useTable(table) {
        const expandRows = computed(() => {
            return makeRows(table.value.list, table.value.expanded, 0)
        })
        return {
            expandRows,
        }
    }
    

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

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

最终实现

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

加载评论