Tree

QTree 是一个高度可定制的用于展示带层级数据的组件,例如树状结构的目录。

QTree API

QTree API


tick-strategy
: String
说明
选择节点的策略类型
no-selection-unset
: Boolean
v2.4.10+
说明
点击当前已选节点时不允许取消选择
default-expand-all
: Boolean
说明
当首次渲染时,允许树展开所有分支。
accordion
: Boolean
说明
允许将树设置为手风琴模式
no-transition
: Boolean
v2.9.2+
说明
在展开/折叠节点时关闭过渡效果;作为副作用,还大幅提升了性能;推荐用于大型树结构。

用法

基础



<template>
  <div class="q-pa-md q-gutter-sm">
    <q-tree
      :nodes="simple"
      node-key="label"
    />
  </div>
</template>

没有连接线



<template>
  <div class="q-pa-md q-gutter-sm">
    <q-tree
      :nodes="simple"
      node-key="label"
      no-connectors
      v-model:expanded="expanded"
    />
  </div>
</template>

Quality ingredients
Good recipe

紧凑的
v2.2.4+



<template>
  <div class="q-pa-md q-gutter-sm">
    <q-tree
      :nodes="simple"
      dense
      node-key="label"
      v-model:expanded="expanded"
    />
  </div>
</template>

Quality ingredients
Good recipe

黑色模式



<template>
  <div class="q-pa-md bg-grey-10 text-white">
    <q-tree
      :nodes="simple"
      node-key="label"
      v-model:expanded="expanded"
      dark
    />
  </div>
</template>

Quality ingredients
Good recipe

集成示例



<template>
  <div>
    <q-splitter
      v-model="splitterModel"
      style="height: 400px"
    >

      <template v-slot:before>
        <div class="q-pa-md">
          <q-tree
            :nodes="simple"
            node-key="label"
            selected-color="primary"
            v-model:selected="selected"
            default-expand-all
          />
        </div>
      </template>

      <template v-slot:after>
        <q-tab-panels
          v-model="selected"
          animated
          transition-prev="jump-up"
          transition-next="jump-up"
        >
          <q-tab-panel name="Relax Hotel">
            <div class="text-h4 q-mb-md">Welcome</div>
            <p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quis praesentium cumque magnam odio iure quidem, quod illum numquam possimus obcaecati commodi minima assumenda consectetur culpa fuga nulla ullam. In, libero.</p>
            <p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quis praesentium cumque magnam odio iure quidem, quod illum numquam possimus obcaecati commodi minima assumenda consectetur culpa fuga nulla ullam. In, libero.</p>
          </q-tab-panel>

          <q-tab-panel name="Food">
            <div class="text-h4 q-mb-md">Food</div>
            <p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quis praesentium cumque magnam odio iure quidem, quod illum numquam possimus obcaecati commodi minima assumenda consectetur culpa fuga nulla ullam. In, libero.</p>
            <p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quis praesentium cumque magnam odio iure quidem, quod illum numquam possimus obcaecati commodi minima assumenda consectetur culpa fuga nulla ullam. In, libero.</p>
          </q-tab-panel>

          <q-tab-panel name="Room service">
            <div class="text-h4 q-mb-md">Room service</div>
            <p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quis praesentium cumque magnam odio iure quidem, quod illum numquam possimus obcaecati commodi minima assumenda consectetur culpa fuga nulla ullam. In, libero.</p>
            <p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quis praesentium cumque magnam odio iure quidem, quod illum numquam possimus obcaecati commodi minima assumenda consectetur culpa fuga nulla ullam. In, libero.</p>
            <p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quis praesentium cumque magnam odio iure quidem, quod illum numquam possimus obcaecati commodi minima assumenda consectetur culpa fuga nulla ullam. In, libero.</p>
          </q-tab-panel>

          <q-tab-panel name="Room view">
            <div class="text-h4 q-mb-md">Room view</div>
            <p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quis praesentium cumque magnam odio iure quidem, quod illum numquam possimus obcaecati commodi minima assumenda consectetur culpa fuga nulla ullam. In, libero.</p>
            <p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quis praesentium cumque magnam odio iure quidem, quod illum numquam possimus obcaecati commodi minima assumenda consectetur culpa fuga nulla ullam. In, libero.</p>
          </q-tab-panel>
        </q-tab-panels>
      </template>
    </q-splitter>
  </div>
</template>

Food

Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quis praesentium cumque magnam odio iure quidem, quod illum numquam possimus obcaecati commodi minima assumenda consectetur culpa fuga nulla ullam. In, libero.

Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quis praesentium cumque magnam odio iure quidem, quod illum numquam possimus obcaecati commodi minima assumenda consectetur culpa fuga nulla ullam. In, libero.

更多信息:QSplitterQTabPanels

自定义内容

注意下面的示例中,使用 header 和 body 的默认插槽实现了自定义内容。



<template>
  <div class="q-pa-md q-gutter-sm">
    <q-tree
      :nodes="customize"
      node-key="label"
      default-expand-all
    >
      <template v-slot:default-header="prop">
        <div class="row items-center">
          <q-icon :name="prop.node.icon || 'share'" color="orange" size="28px" class="q-mr-sm" />
          <div class="text-weight-bold text-primary">{{ prop.node.label }}</div>
        </div>
      </template>

      <template v-slot:default-body="prop">
        <div v-if="prop.node.story">
          <span class="text-weight-bold">This node has a story</span>: {{ prop.node.story }}
        </div>
        <span v-else class="text-weight-light text-black">This is some default content.</span>
      </template>
    </q-tree>
  </div>
</template>

This is some default content.
This is some default content.
Quality ingredients
This node has a story: Lorem ipsum dolor sit amet.
Good recipe
This node has a story: A Congressman works with his equally conniving wife to exact revenge on the people who betrayed him.
This is some default content.
Prompt attention
This is some default content.
Professional waiter
This is some default content.
This is some default content.
Happy atmosphere
This is some default content.
Good table presentation
This is some default content.
Pleasing decor
This is some default content.

注意下面的示例中,使用 header 和 body 插槽实现了自定义内容。



<template>
  <div class="q-pa-md q-gutter-sm">
    <q-tree
      :nodes="customize"
      node-key="label"
      default-expand-all
    >
      <template v-slot:header-root="prop">
        <div class="row items-center">
          <img src="https://cdn.quasar.dev/logo-v2/svg/logo.svg" class="q-mr-sm" style="width:50px;height:50px">
          <div>
            {{ prop.node.label }}
            <q-badge color="orange" class="q-ml-sm">New!</q-badge>
          </div>
        </div>
      </template>

      <template v-slot:header-generic="prop">
        <div class="row items-center">
          <q-icon :name="prop.node.icon || 'star'" color="orange" size="28px" class="q-mr-sm" />
          <div class="text-weight-bold text-primary">{{ prop.node.label }}</div>
        </div>
      </template>

      <template v-slot:body-story="prop">
        <span class="text-weight-thin">The story is:</span> {{ prop.node.story }}
      </template>

      <template v-slot:body-toggle="prop">
        <p class="text-caption">{{ prop.node.caption }}</p>
        <q-toggle v-model="prop.node.enabled" label="I agree to the terms and conditions" />
      </template>
    </q-tree>
  </div>
</template>

Quality ingredients
The story is: Lorem ipsum dolor sit amet.
Good recipe
The story is: A Congressman works with his equally conniving wife to exact revenge on the people who betrayed him.

Why are we as consumers so captivated by stories of great customer service? Perhaps it is because...

I agree to the terms and conditions
Prompt attention
Professional waiter
Happy atmosphere
Good table presentation
Pleasing decor

WARNING

在自定义头部上点击或按下 SPACEENTER 会选中树的选项,并且自定义头部会失焦。

如果您不想要这个行为,只需要使用 <div @click.stop @keypress.stop> 包裹自定义头部的内容即可(或者添加监听事件到相应的组件/元素)。

手风琴,筛选和可选中的

在下面的示例中,当一个节点扩展时,兄弟姐妹节点会收缩。



<template>
  <div class="q-pa-md q-gutter-sm">
    <q-tree
      :nodes="simple"
      accordion
      node-key="label"
      v-model:expanded="expanded"
    />
  </div>
</template>

Quality ingredients
Good recipe


<template>
  <div class="q-pa-md q-gutter-sm">
    <q-input ref="filterRef" filled v-model="filter" label="Filter">
      <template v-slot:append>
        <q-icon v-if="filter !== ''" name="clear" class="cursor-pointer" @click="resetFilter" />
      </template>
    </q-input>

    <q-tree
      :nodes="simple"
      node-key="label"
      :filter="filter"
      default-expand-all
    />
  </div>
</template>

Quality ingredients
Good recipe
Happy atmosphere
Good table presentation
Pleasing decor


<template>
  <div class="q-pa-md q-gutter-sm">
    <div>
      <div class="q-gutter-sm">
        <q-btn size="sm" color="primary" @click="selectGoodService" label="Select 'Good service'" />
        <q-btn v-if="selected" size="sm" color="red" @click="unselectNode" label="Unselect node" />
      </div>
    </div>
    <q-tree
      :nodes="props"
      default-expand-all
      v-model:selected="selected"
      node-key="label"
    />
  </div>
</template>

懒加载



<template>
  <div class="q-pa-md q-gutter-sm">
    <q-tree
      :nodes="lazy"
      default-expand-all
      node-key="label"
      @lazy-load="onLazyLoad"
    />
  </div>
</template>

选中 vs 打勾,展开

  • 选中 (QTree 的 selected 属性) 指向当前选中的节点,在背景颜色上会有不同的区分。
  • 打勾 (QTree 的 ticked 属性) 指向与每个节点关联的复选框
  • 展开 (QTree 的 expanded 属性) 指向已经展开的节点。

为了保证组件正确工作,上述属性都需要使用 v-model:<prop_name> 指令进行动态绑定,(示例:v-model:expanded)。



<template>
  <div class="q-pa-md row q-col-gutter-sm">
    <q-tree class="col-12 col-sm-6"
      :nodes="simple"
      node-key="label"
      tick-strategy="leaf"
      v-model:selected="selected"
      v-model:ticked="ticked"
      v-model:expanded="expanded"
    />
    <div class="col-12 col-sm-6 q-gutter-sm">
      <div class="text-h6">Selected</div>
      <div>{{ selected }}</div>

      <q-separator spaced />

      <div class="text-h6">Ticked</div>
      <div>
        <div v-for="tick in ticked" :key="`ticked-${tick}`">
          {{ tick }}
        </div>
      </div>

      <q-separator spaced />

      <div class="text-h6">Expanded</div>
      <div>
        <div v-for="expand in expanded" :key="`expanded-${expand}`">
          {{ expand }}
        </div>
      </div>
    </div>
  </div>
</template>

Good service (disabled node)
Selected
Pleasant surroundings

Ticked
Quality ingredients
Good table presentation

Expanded
Satisfied customers
Good service (disabled node)
Pleasant surroundings

勾选策略

有三种勾选策略:‘leaf’, ‘leaf-filtered’, ‘strict’,另外一个(默认)‘none’ 将禁用勾选。

策略描述
leaf勾选得到的数据中只会包含叶子节点,勾选一个节点会影响其父节点和子节点(父节点会变成部分勾选或者勾选状态,所有的子节点都会变成勾选状态)
leaf-filteredleaf 策略一样,但是此策略只会应用于筛选后的节点(筛选后仍然显示的节点)。
strict勾选节点不会影响它的父节点或者子节点。

您可以为 QTree 应用一个全局的勾选策略,也可以在 nodes 的 model 中声明 tickStrategy 属性来修改某个特殊节点的勾选策略。



<template>
  <div class="q-pa-md row q-col-gutter-sm">
    <q-tree class="col-12 col-sm-6"
      :nodes="simple"
      v-model:ticked="ticked"
      v-model:expanded="expanded"
      node-key="label"
      :tick-strategy="tickStrategy"
      default-expand-all
    />
    <div class="col-12 col-sm-6">
      <q-option-group
        v-model="tickStrategy"
        :options="tickStrategies"
      />

      <div class="text-h6 q-mt-md">Ticked</div>
      <div>
        <div v-for="tick in ticked" :key="`ticked-${tick}`">
          {{ tick }}
        </div>
      </div>
    </div>
  </div>
</template>

Quality ingredients
Good recipe
Happy atmosphere (*)
Good table presentation
Pleasing decor (*)
Ticked
Pleasant surroundings

自定义筛选方法

您可以通过 filter-method 属性来自定义筛选方法。下面的示例中,使用输入框中的数据和节点标签中的 ‘(*)’ 作为筛选条件:



<template>
  <div class="q-pa-md q-gutter-sm">
    <q-input
      ref="filterRef"
      filled
      v-model="filter"
      label="Search - only filters labels that have also '(*)'"
    >
      <template v-slot:append>
        <q-icon v-if="filter !== ''" name="clear" class="cursor-pointer" @click="resetFilter" />
      </template>
    </q-input>

    <q-tree
      :nodes="simple"
      node-key="label"
      :filter="filter"
      :filter-method="myFilterMethod"
      v-model:expanded="expanded"
      default-expand-all
    />
  </div>
</template>

Good service (disabled node) (*)
Pleasing decor (*)

节点的模型结构

以下描述了 QTree 的 v-model 所考虑的节点的属性。

属性类型不存在时的行为描述
<nodeKey>String, Number生成一个错误节点的 key,key 将会从 nodeKey 属性声明的字段中获取
labelString该项没有标签节点的标签,如果设置了 labelKey 属性,那么将使用对应的字段。
iconString使用默认图标节点的图标。
iconColorString使用继承来的颜色节点图标的颜色,是 Quasar 调色盘其中之一。
imgString不展示图片节点的图片,使用 /public 目录下的图片,例如:‘mountains.png’。
avatarString不显示头像节点的头像,使用 /public 目录下的图片,例如:‘boy-avatar.png’。
childrenArray这个节点不会有子节点一组节点作为子节点。
disabledBoolean这个节点会被启用是否禁用此节点?
expandableBoolean此节点可展开节点是否可展开?
selectableBoolean此节点可选择节点是否可选?
handlerFunction不会调用额外的函数当点击节点时会被调用的函数。接受 node 作为参数
tickableBoolean此节点是否可勾选按照勾选策略来定当使用一个勾选策略时,每个节点都会显示一个勾选框,是否禁用此节点的勾选框?
noTickBoolean节点会显示一个勾选框当使用一个勾选策略时,此节点是否显示勾选框?
tickStrategyString勾选策略使用 ‘none’为此节点重写全局的勾选策略,可选值为 ‘leaf’,‘leaf-filtered’,‘strict’,‘none’。
lazyBoolean子节点不会懒加载子节点是否懒加载?此用例不要声明 ‘children’ 属性。
headerString‘default-header’ 插槽会被使用此节点头部插槽的名称,不需要 ‘header-’ 前缀。示例:‘story’ 指向 ‘header-story’ 插槽。
bodyString‘default-body’ 会被使用此节点 body 插槽的名称,不需要 ‘body-’ 前缀。示例:‘story’ 指向 ‘body-story’ 插槽。

类型定义

查看类型定义
/**
 * Node type to be used with QTree's `nodes` prop
 *
 * @see https:
 *
 * @template TExtra Object type to add extra properties for the node, overrides the existing ones as well
 *
 * @example
 * Basic usage
 * ```ts
 * const nodes: QTreeNode[] = [
 *   
 * ];
 * 
 * ```
 *
 * @example
 * Making some properties required
 * ```ts
 * 
 * const nodes: QTreeNode<{ label: string; icon: string }>[] = [
 *   
 * ];
 * ```
 *
 * @example
 * Adding extra properties
 * ```ts
 * 
 * const nodes: QTreeNode<{ foo: number }>[] = [
 *   
 * ];
 * ```
 *
 * @example
 * Using different label/children properties
 * ```ts
 * type Node = QTreeNode<{ name: string; subNodes: Node[] }>;
 * const nodes: Node[] = [
 *   
 * ];
 * 
 * ```
 *
 * @example
 * Using a different child node type
 * ```ts
 * type ChildNode = QTreeNode<{ foo: number }>;
 * type ParentNode = QTreeNode<{ bar: string; children?: ChildNode[] }>;
 *
 * const nodes: ParentNode[] = [
 *   
 * ];
 * ```
 *
 * @example
 * A very basic file system tree
 * ```ts
 * interface FileInfo {
 *   path: string;
 *   size: number;
 *   lastModified: number;
 * }
 * type FileNode = QTreeNode<FileInfo & { type: "file", children?: never }>;
 * type DirectoryNode = QTreeNode<FileInfo & { type: "directory", children?: (FileNode | DirectoryNode)[] }>;
 *
 * const nodes: DirectoryNode[] = [
 *   {
 *     type: "directory",
 *     path: "/",
 *     size: 0,
 *     lastModified: 0,
 *     
 *     children: [
 *       {
 *         type: "file",
 *         path: "/foo.txt",
 *         size: 100,
 *         lastModified: 1000,
 *         
 *       },
 *       {
 *         type: "directory",
 *         path: "/bar",
 *         size: 0,
 *         lastModified: 0,
 *         
 *       }
 *     ]
 *   }
 * ]
 * ```
 */
export type QTreeNode<TExtra = unknown> = Omit<
  {
    label?: string;
    icon?: string;
    iconColor?: string;
    img?: string;
    avatar?: string;
    children?: QTreeNode<TExtra>[];
    disabled?: boolean;
    expandable?: boolean;
    selectable?: boolean;
    handler?: (node: QTreeNode<TExtra>) => void;
    tickable?: boolean;
    noTick?: boolean;
    tickStrategy?: "leaf" | "leaf-filtered" | "string" | "none";
    lazy?: boolean;
    header?: string;
    body?: string;
  },
  unknown extends TExtra ? "" : keyof TExtra
> &
  (unknown extends TExtra ? Record<string, any> : TExtra);

export interface QTreeLazyLoadParams<
  Node extends QTreeNode = QTreeNode,
  UpdatedNodes extends QTreeNode = Node,
> {
  node: Node;
  key: string;
  done: (nodes: UpdatedNodes[]) => void;
  fail: () => void;
}