@@ -0,0 +1,301 @@
< script lang = "ts" setup >
import type { Arrayable } from '@vueuse/core' ;
import type { FlattenedItem } from 'radix-vue' ;
import type { ClassType , Recordable } from '@vben-core/typings' ;
import { onMounted , ref , watch , watchEffect } from 'vue' ;
import { ChevronRight , IconifyIcon } from '@vben-core/icons' ;
import { cn , get } from '@vben-core/shared/utils' ;
import { useVModel } from '@vueuse/core' ;
import { TreeItem , TreeRoot } from 'radix-vue' ;
import { Checkbox } from '../checkbox' ;
interface TreeProps {
/** 单选时允许取消已有选项 */
allowClear ? : boolean ;
/** 显示边框 */
bordered ? : boolean ;
/** 取消父子关联选择 */
checkStrictly ? : boolean ;
/** 子级字段名 */
childrenField ? : string ;
/** 默认展开的键 */
defaultExpandedKeys ? : Array < number | string > ;
/** 默认展开的级别(优先级高于defaultExpandedKeys) */
defaultExpandedLevel ? : number ;
/** 默认值 */
defaultValue ? : Arrayable < number | string > ;
/** 禁用 */
disabled ? : boolean ;
/** 自定义节点类名 */
getNodeClass ? : ( item : FlattenedItem < Recordable < any > > ) => string ;
iconField ? : string ;
/** label字段 */
labelField ? : string ;
/** 当前值 */
modelValue ? : Arrayable < number | string > ;
/** 是否多选 */
multiple ? : boolean ;
/** 显示由iconField指定的图标 */
showIcon ? : boolean ;
/** 启用展开收缩动画 */
transition ? : boolean ;
/** 树数据 */
treeData : Recordable < any > [ ] ;
/** 值字段 */
valueField ? : string ;
}
const props = withDefaults ( defineProps < TreeProps > ( ) , {
allowClear : false ,
bordered : false ,
checkStrictly : false ,
defaultExpandedKeys : ( ) => [ ] ,
disabled : false ,
expanded : ( ) => [ ] ,
iconField : 'icon' ,
labelField : 'label' ,
modelValue : ( ) => [ ] ,
multiple : false ,
showIcon : true ,
transition : false ,
valueField : 'value' ,
childrenField : 'children' ,
} ) ;
const emits = defineEmits < {
expand : [ value : FlattenedItem < Recordable < any > > ] ;
select : [ value : FlattenedItem < Recordable < any > > ] ;
'update:modelValue' : [ value : Arrayable < Recordable < any > > ] ;
} > ( ) ;
interface InnerFlattenItem < T = Recordable < any > > {
hasChildren : boolean ;
level : number ;
value : T ;
}
function flatten < T = Recordable < any > > (
items : T [ ] ,
childrenField : string = 'children' ,
level = 0 ,
) : InnerFlattenItem < T > [ ] {
const result : InnerFlattenItem < T > [ ] = [ ] ;
items . forEach ( ( item ) => {
const children = get ( item , childrenField ) as Array < T > ;
const val = {
hasChildren : Array . isArray ( children ) && children . length > 0 ,
level ,
value : item ,
} ;
result . push ( val ) ;
if ( val . hasChildren )
result . push ( ... flatten ( children , childrenField , level + 1 ) ) ;
} ) ;
return result ;
}
const flattenData = ref < Array < InnerFlattenItem > > ( [ ] ) ;
const modelValue = useVModel ( props , 'modelValue' , emits , {
deep : true ,
defaultValue : props . defaultValue ? ? [ ] ,
passive : ( props . modelValue === undefined ) as false ,
} ) ;
const expanded = ref < Array < number | string > > ( props . defaultExpandedKeys ? ? [ ] ) ;
const treeValue = ref ( ) ;
onMounted ( ( ) => {
watchEffect ( ( ) => {
flattenData . value = flatten ( props . treeData , props . childrenField ) ;
updateTreeValue ( ) ;
if (
props . defaultExpandedLevel !== undefined &&
props . defaultExpandedLevel > 0
)
expandToLevel ( props . defaultExpandedLevel ) ;
} ) ;
} ) ;
function getItemByValue ( value : number | string ) {
return flattenData . value . find (
( item ) => get ( item . value , props . valueField ) === value ,
) ? . value ;
}
function updateTreeValue ( ) {
const val = modelValue . value ;
treeValue . value = Array . isArray ( val )
? val . map ( ( v ) => getItemByValue ( v ) )
: getItemByValue ( val ) ;
}
watch (
modelValue ,
( ) => {
updateTreeValue ( ) ;
} ,
{ deep : true , immediate : true } ,
) ;
function updateModelValue ( val : Arrayable < Recordable < any > > ) {
modelValue . value = Array . isArray ( val )
? val . map ( ( v ) => get ( v , props . valueField ) )
: get ( val , props . valueField ) ;
}
function expandToLevel ( level : number ) {
const keys : string [ ] = [ ] ;
flattenData . value . forEach ( ( item ) => {
if ( item . level <= level - 1 ) {
keys . push ( get ( item . value , props . valueField ) ) ;
}
} ) ;
expanded . value = keys ;
}
function collapseNodes ( value : Arrayable < number | string > ) {
const keys = new Set ( Array . isArray ( value ) ? value : [ value ] ) ;
expanded . value = expanded . value . filter ( ( key ) => ! keys . has ( key ) ) ;
}
function expandNodes ( value : Arrayable < number | string > ) {
const keys = [ ... ( Array . isArray ( value ) ? value : [ value ] ) ] ;
keys . forEach ( ( key ) => {
if ( expanded . value . includes ( key ) ) return ;
const item = getItemByValue ( key ) ;
if ( item ) {
expanded . value . push ( key ) ;
}
} ) ;
}
function expandAll ( ) {
expanded . value = flattenData . value
. filter ( ( item ) => item . hasChildren )
. map ( ( item ) => get ( item . value , props . valueField ) ) ;
}
function collapseAll ( ) {
expanded . value = [ ] ;
}
function onToggle ( item : FlattenedItem < Recordable < any > > ) {
emits ( 'expand' , item ) ;
}
function onSelect ( item : FlattenedItem < Recordable < any > > ) {
emits ( 'select' , item ) ;
}
defineExpose ( {
collapseAll ,
collapseNodes ,
expandAll ,
expandNodes ,
expandToLevel ,
getItemByValue ,
} ) ;
< / script >
< template >
< TreeRoot
: get -key = " ( item ) = > get ( item , valueField ) "
:get-children=" ( item ) => get ( item , childrenField ) "
:items=" treeData "
:model-value=" treeValue "
v-model:expanded=" expanded as string [ ] "
:default-expanded=" defaultExpandedKeys as string [ ] "
:propagate-select=" ! checkStrictly "
:multiple=" multiple "
:disabled=" disabled "
:selection-behavior=" allowClear || multiple ? 'toggle' : 'replace' "
@update:model-value=" updateModelValue "
v-slot=" { flattenItems } "
:class="
cn (
'text-blackA11 select-none list-none rounded-lg p-2 text-sm font-medium' ,
$attrs . class as unknown as ClassType ,
bordered ? 'border' : '' ,
)
"
>
<div class=" w - full " v-if=" $slots . header ">
<slot name=" header "> </slot>
</div>
<TreeItem
v-for=" item in flattenItems "
v-slot=" {
isExpanded ,
isSelected ,
isIndeterminate ,
handleSelect ,
handleToggle ,
} "
:key=" item . _id "
:style=" { 'padding-left' : ` ${ item . level - 0.5 } rem ` } "
:class="
cn ( 'cursor-pointer' , getNodeClass ? . ( item ) , {
'data-[selected]:bg-accent' : ! multiple ,
} )
"
v-bind=" item . bind "
@select="
( event ) => {
if ( event . detail . originalEvent . type === 'click' ) {
// event.preventDefault();
}
onSelect ( item ) ;
}
"
@toggle="
( event ) => {
if ( event . detail . originalEvent . type === 'click' ) {
event . preventDefault ( ) ;
}
onToggle ( item ) ;
}
"
class=" tree - node focus : ring - grass8 my - 0.5 flex items - center rounded px - 2 py - 1 outline - none focus : ring - 2 "
>
<ChevronRight
v-if=" item . hasChildren "
class=" size - 4 cursor - pointer transition "
:class=" { 'rotate-90' : isExpanded } "
@click.stop=" handleToggle "
/>
<div v-else class=" h - 4 w - 4 ">
<!-- <IconifyIcon v-if=" item . value . icon " :icon=" item . value . icon " /> -->
</div>
<Checkbox
v-if=" multiple "
:checked=" isSelected "
:indeterminate=" isIndeterminate "
@click.stop=" handleSelect "
/>
<div
class=" flex items - center gap - 1 pl - 2 "
@click="
( $event ) => {
$event . stopPropagation ( ) ;
$event . preventDefault ( ) ;
handleSelect ( ) ;
}
"
>
<slot name=" node " v-bind=" item ">
<IconifyIcon
class=" size - 4 "
v-if=" showIcon && get ( item . value , iconField ) "
:icon=" get ( item . value , iconField ) "
/>
{{ get(item.value, labelField) }}
</slot>
</div>
</TreeItem>
<div class=" w - full " v-if=" $slots . footer ">
<slot name=" footer " > < / slot >
< / div >
< / TreeRoot >
< / template >