Demo 总览(可交互)
以下用例与 Playground 使用同一批组件源码(playground/components/demos/),可直接交互。
基础用法
基础的树形结构展示,默认方式垂直方向。
data,默认自上而下(direction="vertical")渲染。 <template>
<vue-okr-tree :data="testData" />
</template>
<script setup>
import { ref } from 'vue'
import { VueOkrTree } from 'vue3-okr-tree'
import 'vue3-okr-tree/dist/style.css'
const testData = ref([{
label: 'xxx科技有有限公司',
children: [{
label: '产品研发部',
children: [{ label: '研发-前端' }, { label: '研发-后端' }, { label: 'UI 设计' }]
}, {
label: '销售部',
children: [{ label: '销售一部' }, { label: '销售二部' }]
}, {
label: '财务部'
}]
}])
</script>水平方向
基础的树形结构展示,自左向右水平方向。
direction 设置为 horizontal 来展示水平方向。 <template>
<vue-okr-tree :data="testData" direction="horizontal" />
</template>
<script setup>
import { ref } from 'vue'
import { VueOkrTree } from 'vue3-okr-tree'
const testData = ref([{
label: 'xxx科技有有限公司',
children: [{
label: '产品研发部',
children: [{ label: '研发-前端' }, { label: '研发-后端' }, { label: 'UI 设计' }]
}, {
label: '销售部',
children: [{ label: '销售一部' }, { label: '销售二部' }]
}, {
label: '财务部'
}]
}])
</script>节点是否可被展开
节点可被展开,默认是不展开。
show-collapsable 设置节点可被展开;此时节点默认折叠,点击圆形 +/- 按钮展开或收起。键盘操作(Vue 3 版新增,符合 WAI-ARIA tree 模式):Tab 进入树后,↑/↓ 在可见节点间移动,→ 展开或进入子节点,← 收起或回到父节点,Enter/Space 选中,Home/End 到首尾;OKR 模式下根节点 ← 进入左子树,左树节点方向键镜像。 <template>
<vue-okr-tree :data="testData" direction="horizontal" show-collapsable />
</template>
<script setup>
import { ref } from 'vue'
import { VueOkrTree } from 'vue3-okr-tree'
const testData = ref([{
label: 'xxx科技有有限公司',
children: [{
label: '产品研发部',
children: [{ label: '研发-前端' }, { label: '研发-后端' }, { label: 'UI 设计' }]
}, {
label: '销售部',
children: [{ label: '销售一部' }, { label: '销售二部' }]
}, {
label: '财务部'
}]
}])
</script>节点默认全部展开
default-expand-all 默认展开所有节点,该参数只有在 show-collapsable 为 true 时有效(未开启 show-collapsable 时组件本身就是全部展开的)。 <template>
<vue-okr-tree
:data="testData"
direction="horizontal"
show-collapsable
default-expand-all
/>
</template>
<script setup>
import { ref } from 'vue'
import { VueOkrTree } from 'vue3-okr-tree'
const testData = ref([{
label: 'xxx科技有有限公司',
children: [{
label: '产品研发部',
children: [{ label: '研发-前端' }, { label: '研发-后端' }, { label: 'UI 设计' }]
}, {
label: '销售部',
children: [{ label: '销售一部' }, { label: '销售二部' }]
}, {
label: '财务部'
}]
}])
</script>可将 Tree 的某些节点设置为默认展开
default-expanded-keys 设置默认展开的节点(本例展开 id 为 5 的「UI 设计」,其祖先节点会一并展开)。需要注意的是,此时必须设置 node-key,其值为节点数据中的一个字段名,该字段在整棵树中是唯一的。 <template>
<vue-okr-tree
:data="testData"
direction="horizontal"
show-collapsable
node-key="id"
:default-expanded-keys="[5]"
/>
</template>
<script setup>
import { ref } from 'vue'
import { VueOkrTree } from 'vue3-okr-tree'
const testData = ref([{
id: 1, label: 'xxx科技有有限公司',
children: [{
id: 2, label: '产品研发部',
children: [{ id: 3, label: '研发-前端' }, { id: 4, label: '研发-后端' }, { id: 5, label: 'UI 设计' }]
}, {
id: 6, label: '销售部',
children: [{ id: 7, label: '销售一部' }, { id: 8, label: '销售二部' }]
}, {
id: 9, label: '财务部'
}]
}])
</script>节点的样式
可自行设置节点的默认样式、选中的样式,以及节点的宽高。
label-class-name 设置节点的样式,支持字符和函数方式;通过 current-lable-class-name 设置当前节点选中的样式,支持字符和函数方式。 label-width / label-height 为 number 时单位 px,为 string 时直接作为 style 值。函数形式的参数是内部 Node 实例(源数据在 node.data)。 <template>
<vue-okr-tree
:data="testData"
direction="horizontal"
show-collapsable
default-expand-all
:label-width="140"
:label-class-name="renderLabelClass"
:current-lable-class-name="renderCurrentClass"
/>
</template>
<script setup>
import { ref } from 'vue'
import { VueOkrTree } from 'vue3-okr-tree'
const testData = ref([{
label: 'xxx科技有有限公司',
children: [{
label: '产品研发部',
children: [{ label: '研发-前端' }, { label: '研发-后端' }, { label: 'UI 设计' }]
}, {
label: '销售部',
children: [{ label: '销售一部' }, { label: '销售二部' }]
}, {
label: '财务部'
}]
}])
// 参数 node 为内部 Node 实例,可按 node.data / node.level 等返回不同 class
const renderLabelClass = (node) => 'label-class-blue'
const renderCurrentClass = (node) => 'label-bg-blue'
</script>
<style>
.label-class-blue { color: #1989fa; }
.label-bg-blue { background: #1989fa; color: #fff; }
</style>节点自定义内容
可自行设置节点内容。
render-content(与原版一致)、node-component(Vue 3 版新增,传一个以 { node, data } 为 props 的组件)、#default 作用域插槽。优先级:插槽 > node-component > render-content。render-content 签名为 (h, node): h 是 Vue 的渲染函数,node 是内部 Node 实例(源数据在 node.data,还可读取 node.isCurrent、 node.expanded 等状态)。也可以改用作用域插槽 #default="{ node, data }"。 <template>
<vue-okr-tree
:data="testData"
direction="horizontal"
show-collapsable
default-expand-all
:render-content="renderContent"
/>
<!-- 等价的 node-component 写法:传组件,props 为 { node, data } -->
<vue-okr-tree :data="testData" direction="horizontal" :node-component="DiyCard" />
<!-- 等价的插槽写法 -->
<vue-okr-tree :data="testData" direction="horizontal">
<template #default="{ node, data }">
<div class="diy-wrapper">
<div class="diy-con-name">{{ data.label }}</div>
<div class="diy-con-content">{{ data.content }}</div>
</div>
</template>
</vue-okr-tree>
</template>
<script setup>
import { ref } from 'vue'
import { VueOkrTree } from 'vue3-okr-tree'
const testData = ref([{
label: 'xxx科技有有限公司', content: '这是一个有活力的公司',
children: [{
label: '产品研发部', content: '这是一个有活力的产品研发部',
children: [
{ label: '研发-前端', content: '这是一个有活力的研发-前端' },
{ label: '研发-后端', content: '这是一个有活力的研发-后端' },
{ label: 'UI 设计', content: '这是一个有活力的UI 设计' }
]
}, {
label: '销售部', content: '这是一个有活力的销售部',
children: [
{ label: '销售一部', content: '这是一个有活力的销售一部' },
{ label: '销售二部', content: '这是一个有活力的销售二部' }
]
}, {
label: '财务部', content: '这是一个有活力的财务部'
}]
}])
// h 由组件传入;node 为内部 Node 实例
function renderContent(h, node) {
return h('div', { class: ['diy-wrapper', node.isCurrent ? 'current-select' : ''] }, [
h('div', { class: 'diy-con-name' }, node.data.label),
h('div', { class: 'diy-con-content' }, node.data.content),
])
}
</script>
<style>
.diy-wrapper { display: flex; flex-direction: column; align-items: flex-start; }
.diy-wrapper .diy-con-name { font-size: 12px; line-height: 18px; color: #646a73; }
.diy-wrapper .diy-con-content { color: #1f2329; line-height: 22px; font-size: 14px; }
.diy-wrapper.current-select .diy-con-name { color: red; }
.diy-wrapper.current-select .diy-con-content { color: #1989fa; }
</style>节点展开按钮自定义内容
可自行设置节点展开按钮内容。
node-btn-content 渲染展开按钮内容,签名同样为 (h, node)。本例把按钮内容替换为文字「智」。 <template>
<vue-okr-tree
:data="testData"
direction="horizontal"
show-collapsable
default-expand-all
:node-btn-content="renderBtnContent"
/>
</template>
<script setup>
import { ref } from 'vue'
import { VueOkrTree } from 'vue3-okr-tree'
const testData = ref([/* ...同上 */])
function renderBtnContent(h, node) {
// 组件自带 .org-chart-node-btn-text 样式:铺满圆形按钮并居中显示
return h('div', { class: 'org-chart-node-btn-text' }, '智')
}
</script>节点动画
该组件内置多种过渡动画,可以直接使用。默认过渡动画是 okr-zoom-in-center,更多动画详见最底部的 API 文档。
animate 属性,通过 animate-name 指定动画的类型,默认动画是 okr-zoom-in-center; animate-duration 控制时长(ms)。切换上方按钮后点击节点的 +/- 按钮观察展开/收起过渡效果。 <template>
<vue-okr-tree
:data="testData"
direction="horizontal"
show-collapsable
default-expand-all
animate
animate-name="okr-zoom-in-top"
:animate-duration="200"
/>
</template>
<script setup>
import { ref } from 'vue'
import { VueOkrTree } from 'vue3-okr-tree'
const testData = ref([{
label: 'xxx科技有有限公司',
children: [{
label: '产品研发部',
children: [{ label: '研发-前端' }, { label: '研发-后端' }, { label: 'UI 设计' }]
}, {
label: '销售部',
children: [{ label: '销售一部' }, { label: '销售二部' }]
}, {
label: '财务部'
}]
}])
</script>OKR 展示模式
该模式的出现,是为了实现跟飞书 OKR 展示的视图一样效果,所以在 Tree 的模式下,扩展成左右两棵子树并根据最长的一个根进行根对齐。
onlyBothTree,以及通过 leftData 表示左子树的结构。Vue 3 版内建 align-root(默认开启,纯 CSS)让每棵树的根节点在自身容器内居中,展开/收起不会位移。 多棵树并排且宽度不足时,用 <OkrTreeGroup> 包裹:它会测量组内所有左子树容器的最大自然宽度并统一设置,使各树根节点水平坐标完全一致——即原版 README 里"结合业务层手动测量 DOM 实现对齐"的正规替代。组件自动响应成员的挂载 / 更新 / 尺寸变化,也可通过 ref.refresh() 手动触发。 <template>
<!-- 多棵树并排对比时用 OkrTreeGroup 包裹,组内根节点严格对齐 -->
<okr-tree-group>
<vue-okr-tree :data="testData" :left-data="testLeftData" only-both-tree direction="horizontal" show-collapsable node-key="id" default-expand-all />
<vue-okr-tree :data="testData" :left-data="testLeftData2" only-both-tree direction="horizontal" show-collapsable node-key="id" default-expand-all />
</okr-tree-group>
</template>
<script setup>
import { ref } from 'vue'
import { VueOkrTree, OkrTreeGroup } from 'vue3-okr-tree'
const testData = ref([{
id: 1, label: 'xxx科技有有限公司',
children: [{
id: 2, label: '产品研发部',
children: [{ id: 3, label: '研发-前端' }, { id: 4, label: '研发-后端' }, { id: 5, label: 'UI 设计' }]
}, {
id: 6, label: '销售部',
children: [{ id: 7, label: '销售一部' }, { id: 8, label: '销售二部' }]
}, {
id: 9, label: '财务部'
}]
}])
const testLeftData = ref([{
id: 1, label: 'xxx科技有有限公司',
children: [{
id: 12, label: '(左)产品研发部',
children: [{ id: 13, label: '(左)研发-前端' }, { id: 14, label: '(左)研发-后端' }, { id: 15, label: '(左)UI 设计' }]
}, {
id: 16, label: '(左)销售部',
children: [{ id: 17, label: '(左)销售一部' }, { id: 18, label: '(左)销售二部' }]
}, {
id: 19, label: '(左)财务部'
}]
}])
// 左侧层级更深的第二棵树(根对齐对比)
const testLeftData2 = ref([
{ id: 1, label: 'xxx科技有有限公司', children: [
{ id: 12, label: '(左)产品研发部', children: [
{ id: 13, label: '(左)研发-前端', children: [
{ id: 131, label: '(左)前端一部' },
{ id: 132, label: '(左)前端二部' }
] },
{ id: 14, label: '(左)研发-后端' },
{ id: 15, label: '(左)UI 设计' }
] },
{ id: 16, label: '(左)销售部', children: [
{ id: 17, label: '(左)销售一部' },
{ id: 18, label: '(左)销售二部' }
] },
{ id: 19, label: '(左)财务部' }
] }
])
</script>OKR 展示模式之自定义节点内容
与上常规 Tree 一样,我们也可以通过自定义渲染函数来制定节点的内容。
render-content 渲染节点内容,通过返回 node 中的 isLeftChild 判断是否是左边的树(本例左树节点加了红色描边)。 <template>
<vue-okr-tree
:data="testData"
:left-data="testLeftData"
only-both-tree
direction="horizontal"
show-collapsable
node-key="id"
label-class-name="no-padding"
default-expand-all
:render-content="renderContent"
/>
</template>
<script setup>
import { ref } from 'vue'
import { VueOkrTree } from 'vue3-okr-tree'
const testData = ref([/* 右树:id + label + content */])
const testLeftData = ref([/* 左树:id + label + content */])
function renderContent(h, node) {
const cls = ['diy-wrapper2']
if (node.isCurrent) cls.push('current-select')
if (node.isLeftChild) cls.push('left-child')
return h('div', { class: cls }, [
h('div', { class: 'diy-con-name' }, node.data.label),
h('div', { class: 'diy-con-content' }, node.data.content),
])
}
</script>
<style>
.diy-wrapper2 { padding: 10px; }
.no-padding { padding: 0 !important; }
.diy-wrapper2.left-child { border: 1px solid red; }
</style>OKR 展示模式显示节点数
通过 show-node-num 可以显示展开的子节点数。
show-node-num 在节点折叠时于圆形按钮内显示子节点数量;配合 node-btn-content 时显示自定义按钮内容。本例先收起几个节点(点击 +/- 按钮)观察数字。 <template>
<vue-okr-tree
:data="testData"
:left-data="testLeftData"
only-both-tree
direction="horizontal"
show-collapsable
node-key="id"
label-class-name="no-padding"
show-node-num
:render-content="renderContent"
:node-btn-content="renderBtnContent"
/>
</template>
<script setup>
import { ref } from 'vue'
import { VueOkrTree } from 'vue3-okr-tree'
const testData = ref([/* 右树 */])
const testLeftData = ref([/* 左树 */])
function renderContent(h, node) { /* 同上一例 */ }
function renderBtnContent(h, node) {
return h('div', { class: 'org-chart-node-btn-text' }, String(node.childNodes.length || 0))
}
</script>节点过滤(不可展开)及支持的方法
通过关键字过滤树节点,并演示通过 ref 调用的全部方法。
filter 方法,参数为关键字。需要注意的是,此时需要设置 filter-node-method,值为过滤函数;清空关键字时会以空值再次调用filter(''),过滤函数需对空值返回 true 才能恢复全部节点。 getNode / setCurrentNode / remove / append / insertBefore / insertAfter / updateKeyChildren 等方法依赖 node-key;增删方法会同步修改传入的源数据。 <template>
<div class="filter-wrapper">
<input v-model="filterText" placeholder="输入关键字进行过滤" />
</div>
<div class="btns-wrap">
<button @click="getNodeById">通过 id 获取销售一部</button>
<button @click="setCurrentNode">通过 node 设置 销售一部 为选中状态</button>
<button @click="remove">删除产品研发部</button>
<button @click="append">为销售部门增加新的部门</button>
<button @click="updateKeyChildren">更新销售部子部门</button>
</div>
<vue-okr-tree
ref="tree"
:data="testData"
current-lable-class-name="crrentClass"
direction="horizontal"
:filter-node-method="filterNode"
node-key="id"
/>
</template>
<script setup>
import { ref, watch } from 'vue'
import { VueOkrTree } from 'vue3-okr-tree'
const tree = ref(null)
const filterText = ref('')
const testData = ref([{
id: 1, label: 'xxx科技有有限公司',
children: [{
id: 2, label: '产品研发部',
children: [{ id: 3, label: '研发-前端' }, { id: 4, label: '研发-后端' }, { id: 5, label: 'UI 设计' }]
}, {
id: 6, label: '销售部',
children: [{ id: 7, label: '销售一部' }, { id: 8, label: '销售二部' }]
}, {
id: 9, label: '财务部'
}]
}])
watch(filterText, (val) => tree.value.filter(val))
// 空值需返回 true,否则清空关键字后节点无法恢复
function filterNode(value, data) {
if (!value) return true
return data.label.indexOf(value) !== -1
}
function getNodeById() {
const node = tree.value.getNode(7)
alert(node.data.label)
}
function setCurrentNode() {
tree.value.setCurrentNode(tree.value.getNode(7))
}
function remove() {
tree.value.remove(tree.value.getNode(2))
}
function append() {
tree.value.append({ id: 10, label: '销售三部' }, tree.value.getNode(6))
}
function updateKeyChildren() {
tree.value.updateKeyChildren(6, [
{ id: 7, label: '销售一部', children: [{ id: 1117, label: '销售一部--子一' }] },
{ id: 8, label: '销售二部' },
{ id: 77, label: '销售三部' },
])
}
</script>
<style>
.crrentClass { color: red; }
</style>OKR 模式节点过滤(可被展开)
OKR 模式下调用 filter 会同时过滤左右两棵子树。
filter 方法,参数为关键字,OKR 模式下左右子树同时过滤。左右两棵树可以存在相同的 id(本例根节点 id 均为 1),getNode 等按 key 查找的方法优先返回右树节点。 <template>
<div class="filter-wrapper">
<input v-model="filterText" placeholder="输入关键字进行过滤" />
</div>
<vue-okr-tree
ref="tree"
:data="testData"
:left-data="testLeftData"
only-both-tree
direction="horizontal"
show-collapsable
node-key="id"
default-expand-all
current-lable-class-name="crrentClass"
:filter-node-method="filterNode"
/>
</template>
<script setup>
import { ref, watch } from 'vue'
import { VueOkrTree } from 'vue3-okr-tree'
const tree = ref(null)
const filterText = ref('')
const testData = ref([/* 右树,见 OKR 展示模式 */])
const testLeftData = ref([/* 左树 */])
// OKR 模式下 filter 会同时过滤左右两棵子树
watch(filterText, (val) => tree.value.filter(val))
function filterNode(value, data) {
if (!value) return true
return data.label.indexOf(value) !== -1
}
</script>支持的事件(不可展开)
不可展开时支持的事件有 节点点击 和 鼠标右键点击。
node-click 回调参数为 (data, node, nodeComponent); node-contextmenu 回调参数为 (event, data, node, nodeComponent)。 只有外部监听了 node-contextmenu 时组件才会阻止浏览器默认右键菜单。 <template>
<vue-okr-tree
:data="testData"
@node-click="handleNodeClick"
@node-contextmenu="handleCoxMenu"
/>
</template>
<script setup>
import { ref } from 'vue'
import { VueOkrTree } from 'vue3-okr-tree'
const testData = ref([{
label: 'xxx科技有有限公司',
children: [{
label: '产品研发部',
children: [{ label: '研发-前端' }, { label: '研发-后端' }, { label: 'UI 设计' }]
}, {
label: '销售部',
children: [{ label: '销售一部' }, { label: '销售二部' }]
}, {
label: '财务部'
}]
}])
function handleNodeClick(data, node, nodeComponent) {
alert(`我是${data.label},我被点击了`)
}
function handleCoxMenu(event, data, node, nodeComponent) {
alert(`我是${data.label},我的右键被点击了`)
}
</script>支持的事件(可被展开)
可展开时支持的事件有 节点点击、鼠标右键点击,节点的展开以及节点的关闭。
onlyBothTree,以及通过 leftData 表示左子树的结构。点击根节点左右两侧的 +/- 按钮会分别触发 node-expand / node-collapse。 <template>
<vue-okr-tree
:data="testData"
:left-data="testLeftData"
only-both-tree
direction="horizontal"
show-collapsable
node-key="id"
default-expand-all
@node-click="handleNodeClick"
@node-contextmenu="handleCoxMenu"
@node-expand="handleNodeExpand"
@node-collapse="handleNodeCollapse"
/>
</template>
<script setup>
import { ref } from 'vue'
import { VueOkrTree } from 'vue3-okr-tree'
const testData = ref([/* 右树 */])
const testLeftData = ref([/* 左树 */])
const handleNodeClick = (data, node) => alert(`我是${data.label},我被点击了`)
const handleCoxMenu = (event, data, node) => alert(`我是${data.label},我的右键被点击了`)
const handleNodeExpand = (data, node) => alert(`我是${data.label},我被展开了`)
const handleNodeCollapse = (data, node) => alert(`我是${data.label},我被收起了`)
</script>受控状态与方法(1.2.0 新增)
用 v-model:expanded-keys / v-model:current-key 双向绑定展开态与选中态; 配合 expandAll / collapseAll / expandNode / collapseNode / scrollToNode 方法,以及 #expand-btn 插槽。
[1]currentKey:nullexpanded-keys 传入后为受控模式:列表内节点展开、其余收起,点击 +/- 或调用展开/收起方法都会触发 update:expandedKeys 回写;未传时保持原版非受控行为。 current-key 同理,null 表示无选中。两者都需要 node-key。#expand-btn 插槽参数为 { node, data, expanded, side },show-node-num 开启时数字优先。 <template>
<button @click="tree.expandAll()">expandAll()</button>
<button @click="tree.collapseAll()">collapseAll()</button>
<button @click="tree.expandNode(5)">expandNode(5)</button>
<button @click="tree.scrollToNode(8)">scrollToNode(8)</button>
<vue-okr-tree
ref="tree"
v-model:expanded-keys="expandedKeys"
v-model:current-key="currentKey"
:data="testData"
direction="horizontal"
show-collapsable
node-key="id"
>
<!-- 自定义展开按钮:side 为 'left' | 'right',expanded 为该侧当前是否展开 -->
<template #expand-btn="{ expanded, side }">
<span class="org-chart-node-btn-text">{{ expanded ? '−' : '+' }}</span>
</template>
</vue-okr-tree>
</template>
<script setup>
import { ref } from 'vue'
import { VueOkrTree } from 'vue3-okr-tree'
const tree = ref(null)
const expandedKeys = ref([1]) // 受控:只展开 id 为 1 的根节点
const currentKey = ref(null) // 受控:无选中
const testData = ref([{
id: 1, label: 'xxx科技有有限公司',
children: [{
id: 2, label: '产品研发部',
children: [{ id: 3, label: '研发-前端' }, { id: 4, label: '研发-后端' }, { id: 5, label: 'UI 设计' }]
}, {
id: 6, label: '销售部',
children: [{ id: 7, label: '销售一部' }, { id: 8, label: '销售二部' }]
}, {
id: 9, label: '财务部'
}]
}])
</script>懒加载子节点
lazy + load(Vue 3 版 1.4.0 新增):初始只给顶层节点,子级在首次展开时通过 load 异步获取(本用例模拟 800ms 接口延迟);resolve 后写入源数据并展开,加载过的节点不会重复请求。 reject 或抛错时节点回到折叠态、可重试;加载中按钮带 is-loading 旋转指示, #expand-btn 作用域新增 loading。
<template>
<vue-okr-tree
:data="lazyData"
node-key="id"
direction="horizontal"
show-collapsable
lazy
:load="loadNode"
/>
</template>
<script setup>
import { ref } from 'vue'
import { VueOkrTree, TreeLoadFunction } from 'vue3-okr-tree'
const lazyData = ref([{ id: 1, label: 'xxx科技有有限公司' }])
// 模拟 800ms 异步接口;reject 或抛错时节点回到折叠态、可重试
const loadNode = (node, resolve, reject) => {
fetchChildren(node.data.id)
.then(resolve)
.catch(() => reject())
}
</script>画布组件 OkrTreeViewport(1.4.0 新增)
<okr-tree-viewport> 包裹树即可获得缩放与平移能力(不侵入树本体):滚轮缩放以指针为中心、按住拖拽平移、双击复位、触控双指捏合。 默认 wheel-behavior="ctrl-zoom"——按住 Ctrl / ⌘ + 滚轮缩放,避免劫持页面滚动;设为 zoom 始终缩放、scroll 从不缩放。
#toolbar 作用域插槽自定义(作用域含 zoom / zoomIn / zoomOut / reset / fit);导出基于 html-to-image(默认动态 import,也可通过 exportImage({ toPng }) 传入渲染函数,本用例即后者)。 centerNode(key) 可先展开祖先再把视口中心对准指定节点。 <template>
<okr-tree-viewport ref="vp" toolbar :wheel-behavior="wheelMode">
<vue-okr-tree :data="orgData" node-key="id" direction="horizontal" show-collapsable />
<template #toolbar="{ zoom, zoomIn, zoomOut, reset, fit }">
<button @click="zoomOut()">−</button>
<span>{{ Math.round(zoom * 100) }}%</span>
<button @click="zoomIn()">+</button>
<button @click="reset()">重置</button>
<button @click="fit()">适应窗口</button>
<button @click="exportPng()">导出 PNG</button>
</template>
</okr-tree-viewport>
</template>
<script setup>
import { ref } from 'vue'
import { toPng } from 'html-to-image'
import { OkrTreeViewport, VueOkrTree } from 'vue3-okr-tree'
const vp = ref(null)
const wheelMode = ref('ctrl-zoom') // ctrl-zoom(默认)/ zoom / scroll
const orgData = ref([{ id: 1, label: 'xxx科技有有限公司', children: [/* ... */] }])
function exportPng() {
// 传入 toPng 可免去动态 import 依赖;不传则按需 import('html-to-image')
vp.value.exportImage({ type: 'png', scale: 2, background: '#fff', toPng })
}
</script>