264 lines
6.0 KiB
Vue
264 lines
6.0 KiB
Vue
<template>
|
|
<div class="cube-tree-item" :style="{ marginLeft: `${depth * config.indentSize}px` }">
|
|
<div
|
|
class="cube-tree-item-header"
|
|
@click="handleClick"
|
|
:class="{
|
|
'has-children': hasChildren,
|
|
'selected': selected,
|
|
'expanded': item.expanded,
|
|
'in-path': inPath
|
|
}"
|
|
>
|
|
<span v-if="config.showExpandIcon && hasChildren" class="expand-icon">
|
|
{{ item.expanded ? '▼' : '▶' }}
|
|
</span>
|
|
<span v-else class="expand-icon placeholder-icon"></span>
|
|
|
|
<slot name="icon" :item="item">
|
|
<span class="item-icon">{{ computedIcon }}</span>
|
|
</slot>
|
|
|
|
<slot name="prefix" :item="item">
|
|
<span v-if="config.showLevel && item.level" class="level-prefix">
|
|
{{ config.levelPrefix }}{{ item.level }}
|
|
</span>
|
|
</slot>
|
|
|
|
<slot name="title" :item="item">
|
|
<span class="item-title">{{ item.title }}</span>
|
|
</slot>
|
|
|
|
<slot name="suffix" :item="item"></slot>
|
|
</div>
|
|
|
|
<transition name="expand">
|
|
<div v-if="item.expanded && hasChildren" class="cube-tree-children">
|
|
<CubeTreeItem
|
|
v-for="child in item.children"
|
|
:key="child.id"
|
|
:item="child"
|
|
:depth="depth + 1"
|
|
:selected="selectedId === child.id"
|
|
:selected-id="selectedId"
|
|
:config="config"
|
|
:icon-map="iconMap"
|
|
:in-path="isInPath(child.id)"
|
|
@node-click="emit('node-click', $event)"
|
|
@toggle-expand="emit('toggle-expand', $event)"
|
|
>
|
|
<template #icon="{ item }">
|
|
<slot name="icon" :item="item"></slot>
|
|
</template>
|
|
<template #title="{ item }">
|
|
<slot name="title" :item="item"></slot>
|
|
</template>
|
|
<template #prefix="{ item }">
|
|
<slot name="prefix" :item="item"></slot>
|
|
</template>
|
|
<template #suffix="{ item }">
|
|
<slot name="suffix" :item="item"></slot>
|
|
</template>
|
|
</CubeTreeItem>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue'
|
|
|
|
const props = defineProps({
|
|
item: Object,
|
|
depth: { type: Number, default: 0 },
|
|
selected: { type: Boolean, default: false },
|
|
selectedId: { type: String, default: '' },
|
|
config: {
|
|
type: Object,
|
|
default: () => ({
|
|
indentSize: 16,
|
|
showExpandIcon: true,
|
|
iconType: 'custom',
|
|
showLevel: false,
|
|
levelPrefix: 'H',
|
|
expandable: true
|
|
})
|
|
},
|
|
iconMap: Object,
|
|
inPath: { type: Boolean, default: false }
|
|
})
|
|
|
|
const emit = defineEmits(['node-click', 'toggle-expand'])
|
|
|
|
const hasChildren = computed(() =>
|
|
props.item.children && props.item.children.length > 0
|
|
)
|
|
|
|
const computedIcon = computed(() => {
|
|
const iconType = props.config.iconType
|
|
const userIconMap = props.iconMap
|
|
|
|
const defaultFileIconMap = {
|
|
'md': '📖',
|
|
'txt': '📝',
|
|
'pdf': '📚',
|
|
'png': '🖼️',
|
|
'jpg': '🖼️',
|
|
'jpeg': '🖼️',
|
|
'gif': '🎞️',
|
|
'svg': '📐',
|
|
'json': '🔧',
|
|
'yaml': '🔧',
|
|
'yml': '🔧',
|
|
'default': '📚',
|
|
'folder': '📁'
|
|
}
|
|
|
|
const defaultHeadingIconMap = {
|
|
1: '📌',
|
|
2: '📎',
|
|
3: '📏',
|
|
4: '📐',
|
|
5: '🔹',
|
|
6: '🔸',
|
|
'default': '📄'
|
|
}
|
|
|
|
if (iconType === 'file') {
|
|
const effectiveIconMap = userIconMap || defaultFileIconMap
|
|
|
|
if (hasChildren.value) {
|
|
return effectiveIconMap.folder || '📁'
|
|
}
|
|
|
|
if (props.item.id) {
|
|
const ext = props.item.id.split('.').pop()?.toLowerCase()
|
|
return effectiveIconMap[ext] || effectiveIconMap.default || '📄'
|
|
}
|
|
|
|
return effectiveIconMap.default || '📄'
|
|
} else if (iconType === 'heading') {
|
|
if (hasChildren.value) {
|
|
if (props.item.level === 1) {
|
|
return '📚'
|
|
}
|
|
return '📖'
|
|
}
|
|
return '📄'
|
|
}
|
|
|
|
return hasChildren.value ? '📁' : '📄'
|
|
})
|
|
|
|
const isInPath = (childId) => {
|
|
if (!props.inPath) return false
|
|
return props.selectedId === childId
|
|
}
|
|
|
|
const handleClick = () => {
|
|
if (hasChildren.value && props.config.expandable) {
|
|
emit('toggle-expand', props.item.id)
|
|
} else {
|
|
emit('node-click', props.item)
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.cube-tree-item {
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.cube-tree-item-header {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 6px 8px;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
transition: background-color 0.2s;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.cube-tree-item-header:hover {
|
|
background-color: var(--cube-tree-item-hover-bg, rgba(0, 0, 0, 0.05));
|
|
}
|
|
|
|
.cube-tree-item-header.selected {
|
|
background-color: var(--cube-tree-item-selected-bg, #1890ff);
|
|
color: var(--cube-tree-item-selected-color, #ffffff);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.cube-tree-item-header.in-path {
|
|
background-color: var(--cube-tree-item-path-bg, rgba(24, 144, 255, 0.1));
|
|
}
|
|
|
|
.cube-tree-item-header.in-path.selected {
|
|
background-color: var(--cube-tree-item-selected-bg, #1890ff);
|
|
}
|
|
|
|
.expand-icon {
|
|
margin-right: 6px;
|
|
font-size: 12px;
|
|
color: var(--cube-tree-item-icon-color, rgba(0, 0, 0, 0.45));
|
|
width: 16px;
|
|
display: inline-block;
|
|
text-align: center;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.cube-tree-item-header.selected .expand-icon {
|
|
color: var(--cube-tree-item-selected-color, #ffffff);
|
|
}
|
|
|
|
.placeholder-icon {
|
|
visibility: hidden;
|
|
}
|
|
|
|
.item-icon {
|
|
margin-right: 6px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.level-prefix {
|
|
margin-right: 6px;
|
|
font-size: 12px;
|
|
color: var(--cube-tree-item-level-color, rgba(0, 0, 0, 0.45));
|
|
font-weight: 600;
|
|
}
|
|
|
|
.cube-tree-item-header.selected .level-prefix {
|
|
color: var(--cube-tree-item-selected-color, #ffffff);
|
|
}
|
|
|
|
.item-title {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: var(--cube-tree-item-title-color, inherit);
|
|
}
|
|
|
|
.cube-tree-children {
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.expand-enter-active,
|
|
.expand-leave-active {
|
|
transition: all 0.3s ease;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.expand-enter-from,
|
|
.expand-leave-to {
|
|
max-height: 0;
|
|
opacity: 0;
|
|
}
|
|
|
|
.expand-enter-to,
|
|
.expand-leave-from {
|
|
max-height: 1000px;
|
|
opacity: 1;
|
|
}
|
|
</style>
|