MianyVue/src/components/ui/ModelTreeNode.vue

229 lines
5.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="tree-node" ref="nodeRef">
<div
class="node-label"
:class="{
'has-children': hasChildren,
'mesh-node': node.type === 'mesh',
'selected': isSelected
}"
@click="toggleExpand"
>
<i
v-if="hasChildren"
class="fas expand-icon"
:class="expanded ? 'fa-chevron-down' : 'fa-chevron-right'"
></i>
<i class="fas node-icon" :class="getIcon()"></i>
<span class="node-name">{{ node.name }}</span>
<span v-if="node.type === 'mesh'" class="mesh-info">
({{ node.triangleCount?.toLocaleString() }} 面)
</span>
</div>
<div v-if="expanded && hasChildren" class="node-children">
<ModelTreeNode
v-for="(child, index) in node.children"
:key="index"
:node="child"
:selected-mesh-id="selectedMeshId"
@mesh-click="(meshId) => emit('mesh-click', meshId)"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, nextTick } from 'vue'
const props = defineProps({
node: {
type: Object,
required: true
},
selectedMeshId: {
type: Object,
default: null
}
})
const emit = defineEmits(['mesh-click'])
const expanded = ref(props.node.expanded || false)
const nodeRef = ref(null)
const hasChildren = computed(() => {
return props.node.children && props.node.children.length > 0
})
const toggleExpand = () => {
// 如果是mesh节点触发点击事件
if (props.node.type === 'mesh') {
const meshId = {
nodeId: props.node.nodeId,
meshIndex: props.node.meshIndex
}
emit('mesh-click', meshId)
return
}
// 否则切换展开状态
if (hasChildren.value) {
expanded.value = !expanded.value
}
}
// 判断当前mesh节点是否被选中
const isSelected = computed(() => {
if (props.node.type !== 'mesh' || !props.selectedMeshId) return false
const result = props.node.nodeId === props.selectedMeshId.nodeId &&
props.node.meshIndex === props.selectedMeshId.meshIndex
if (result) {
console.log('节点匹配成功:', {
nodeName: props.node.name,
nodeId: props.node.nodeId,
meshIndex: props.node.meshIndex,
selectedNodeId: props.selectedMeshId.nodeId,
selectedMeshIndex: props.selectedMeshId.meshIndex
})
}
return result
})
const getIcon = () => {
if (props.node.type === 'mesh') {
return 'fa-cube'
}
return expanded.value ? 'fa-folder-open' : 'fa-folder'
}
// 检查子节点中是否有被选中的mesh
const hasSelectedChild = () => {
if (!props.node.children || !props.selectedMeshId) return false
const checkNode = (node) => {
if (node.type === 'mesh') {
return node.nodeId === props.selectedMeshId.nodeId &&
node.meshIndex === props.selectedMeshId.meshIndex
}
if (node.children) {
return node.children.some(checkNode)
}
return false
}
return props.node.children.some(checkNode)
}
// 监听选中状态变化,自动展开到选中节点
watch(() => props.selectedMeshId, () => {
if (hasSelectedChild()) {
expanded.value = true
}
}, { immediate: true })
// 当节点被选中时,滚动到可视区域
watch(isSelected, async (newVal) => {
if (newVal && nodeRef.value) {
await nextTick()
nodeRef.value.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest'
})
}
})
</script>
<style scoped>
.tree-node {
margin: 2px 0;
}
.node-label {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
border-radius: var(--size-border-radius);
cursor: pointer;
transition: var(--transition-all);
color: var(--color-text-primary);
font-size: var(--font-size-sm);
}
.node-label:hover {
background: var(--color-bg-hover);
}
.node-label.has-children {
font-weight: var(--font-weight-medium);
}
.node-label.mesh-node {
cursor: pointer;
}
.node-label.mesh-node:hover {
background: var(--color-white-rgb-1);
}
.node-label.selected {
background: var(--color-primary-rgb-2);
border-left: 4px solid var(--color-primary);
padding-left: 4px;
font-weight: var(--font-weight-semibold);
color: var(--color-primary);
box-shadow: 0 2px 8px var(--color-primary-rgb-2);
animation: highlight 0.3s ease-in-out;
}
@keyframes highlight {
0% {
transform: translateX(0);
opacity: 0.7;
}
50% {
transform: translateX(2px);
opacity: 1;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
.expand-icon {
width: 12px;
font-size: 10px;
color: var(--color-text-tertiary);
transition: transform 0.2s;
}
.node-icon {
font-size: 14px;
color: var(--color-primary);
width: 16px;
text-align: center;
}
.node-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mesh-info {
color: var(--color-text-tertiary);
font-size: var(--font-size-xs);
margin-left: auto;
flex-shrink: 0;
}
.node-children {
margin-left: 20px;
border-left: 1px solid var(--color-border-primary);
padding-left: 4px;
}
</style>