init3
This commit is contained in:
391
app/components/PowerStation/ModelTreePanel.vue
Normal file
391
app/components/PowerStation/ModelTreePanel.vue
Normal file
@@ -0,0 +1,391 @@
|
||||
<!--
|
||||
ModelTreePanel.vue
|
||||
模型结构树面板组件
|
||||
显示3D模型的层级结构,支持搜索、节点选择和聚焦
|
||||
-->
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="bg-[#0b1120] h-full border-r border-cyan-500/20 flex flex-col transition-all duration-300"
|
||||
>
|
||||
<!-- ============================================================
|
||||
搜索头部
|
||||
============================================================ -->
|
||||
<div class="p-4 bg-[#0f172a] border-b border-cyan-500/20">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h2 class="text-base font-bold text-cyan-400 flex items-center font-mono tracking-wide">
|
||||
<el-icon class="mr-2"><Files /></el-icon>
|
||||
模型结构
|
||||
</h2>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="filterText"
|
||||
placeholder="搜索节点..."
|
||||
prefix-icon="Search"
|
||||
clearable
|
||||
class="tech-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================
|
||||
树内容
|
||||
============================================================ -->
|
||||
<div
|
||||
ref="treeContainer"
|
||||
class="flex-1 overflow-y-auto overflow-x-hidden py-2 custom-scrollbar min-h-0"
|
||||
>
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="store.modelHierarchy"
|
||||
:props="defaultProps"
|
||||
node-key="id"
|
||||
:filter-node-method="filterNodeMethod"
|
||||
highlight-current
|
||||
default-expand-all
|
||||
class="tech-tree"
|
||||
@node-click="handleNodeClick"
|
||||
@node-expand="handleNodeExpand"
|
||||
@node-collapse="handleNodeCollapse"
|
||||
>
|
||||
<template #default="{ node }">
|
||||
<div
|
||||
:id="'tree-node-' + node.data.id"
|
||||
class="flex items-center gap-2 text-sm w-full pr-2 group select-none py-1"
|
||||
@dblclick="handleNodeDblClick(node.data)"
|
||||
@mousedown.prevent="$event.preventDefault()"
|
||||
@selectstart.prevent="$event.preventDefault()"
|
||||
>
|
||||
<!-- 节点图标 -->
|
||||
<el-icon v-if="!node.isLeaf" class="text-cyan-500"><Folder /></el-icon>
|
||||
<el-icon v-else class="text-blue-400"><Document /></el-icon>
|
||||
|
||||
<!-- 节点标签 -->
|
||||
<span
|
||||
class="truncate flex-1 font-mono text-gray-400 group-hover:text-cyan-300 transition-colors"
|
||||
:title="node.label"
|
||||
>
|
||||
{{ node.label }}
|
||||
</span>
|
||||
|
||||
<!-- 聚焦按钮 -->
|
||||
<el-tooltip content="聚焦视图" placement="right" :show-after="500" effect="dark">
|
||||
<el-icon
|
||||
class="text-cyan-500/50 hover:text-cyan-400 cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
@click.stop="handleZoom(node.data)"
|
||||
>
|
||||
<ZoomIn />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* ModelTreePanel 组件
|
||||
* 显示模型的层级结构树,支持搜索、选择和聚焦功能
|
||||
*/
|
||||
import { Document, Files, Folder, ZoomIn } from '@element-plus/icons-vue'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { usePowerStationStore } from '~/stores/powerStation'
|
||||
|
||||
// ============================================================
|
||||
// 状态和引用
|
||||
// ============================================================
|
||||
|
||||
const store = usePowerStationStore()
|
||||
const route = useRoute()
|
||||
|
||||
/** 过滤文本 */
|
||||
const filterText = ref('')
|
||||
/** 树组件引用 */
|
||||
const treeRef = ref()
|
||||
/** 树容器引用 */
|
||||
const treeContainer = ref<HTMLElement>()
|
||||
/** 是否折叠 */
|
||||
const isCollapsed = ref(false)
|
||||
/** 是否为内部选择(来自树本身的选择) */
|
||||
const isInternalSelection = ref(false)
|
||||
/** 已展开的节点ID集合 */
|
||||
const expandedKeys = ref<Set<string>>(new Set())
|
||||
|
||||
/** 树组件属性配置 */
|
||||
const defaultProps = {
|
||||
value: 'id',
|
||||
label: 'label',
|
||||
children: 'children',
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 响应式布局
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 更新布局状态
|
||||
* 根据窗口宽度自动折叠
|
||||
*/
|
||||
const updateLayout = () => {
|
||||
if (window.innerWidth < 1200) {
|
||||
isCollapsed.value = true
|
||||
} else {
|
||||
isCollapsed.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换折叠状态
|
||||
*/
|
||||
const toggleCollapse = () => {
|
||||
isCollapsed.value = !isCollapsed.value
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 过滤功能
|
||||
// ============================================================
|
||||
|
||||
// 监听过滤文本变化
|
||||
watch(filterText, val => {
|
||||
treeRef.value?.filter(val)
|
||||
})
|
||||
|
||||
/**
|
||||
* 节点过滤方法
|
||||
* @param value - 过滤值
|
||||
* @param data - 节点数据
|
||||
* @returns 是否显示该节点
|
||||
*/
|
||||
const filterNodeMethod = (value: string, data: any) => {
|
||||
if (!value) return true
|
||||
const searchValue = value.toLowerCase()
|
||||
const label = data.label?.toLowerCase() || ''
|
||||
return label.includes(searchValue)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 节点操作
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 处理节点点击
|
||||
* @param data - 节点数据
|
||||
*/
|
||||
const handleNodeClick = (data: any) => {
|
||||
// 更新store选择状态
|
||||
store.selectNode(data)
|
||||
// 标记为内部选择
|
||||
isInternalSelection.value = true
|
||||
|
||||
// 移动设备上自动折叠
|
||||
if (window.innerWidth < 1024) {
|
||||
isCollapsed.value = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理缩放/聚焦操作
|
||||
* @param data - 节点数据
|
||||
*/
|
||||
const handleZoom = (data: any) => {
|
||||
store.selectNode(data)
|
||||
store.triggerFocus(data.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理节点双击
|
||||
* @param data - 节点数据
|
||||
*/
|
||||
const handleNodeDblClick = async (data: any) => {
|
||||
await navigateTo({
|
||||
query: {
|
||||
...route.query,
|
||||
currentNodeId: data.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理节点展开
|
||||
* @param data - 节点数据
|
||||
*/
|
||||
const handleNodeExpand = (data: any) => {
|
||||
expandedKeys.value.add(data.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理节点折叠
|
||||
* @param data - 节点数据
|
||||
*/
|
||||
const handleNodeCollapse = (data: any) => {
|
||||
expandedKeys.value.delete(data.id)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 辅助函数
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 查找节点路径
|
||||
* @param nodes - 节点数组
|
||||
* @param targetId - 目标ID
|
||||
* @param path - 当前路径
|
||||
* @returns 路径数组或null
|
||||
*/
|
||||
const findPath = (nodes: any[], targetId: string, path: string[] = []): string[] | null => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === targetId) {
|
||||
return path
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
const result = findPath(node.children, targetId, [...path, node.id])
|
||||
if (result) return result
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 监听器
|
||||
// ============================================================
|
||||
|
||||
// 监听store选择变化(3D视图 -> 树)
|
||||
watch(
|
||||
() => store.currentNodeId,
|
||||
newId => {
|
||||
if (!treeRef.value) return
|
||||
|
||||
// 如果是内部选择,不需要滚动/展开
|
||||
if (isInternalSelection.value) {
|
||||
isInternalSelection.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!newId) {
|
||||
treeRef.value.setCurrentKey(null)
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 高亮节点
|
||||
try {
|
||||
treeRef.value.setCurrentKey(newId)
|
||||
} catch (e) {
|
||||
console.warn('设置当前键失败:', e)
|
||||
}
|
||||
|
||||
// 2. 查找路径并展开/滚动
|
||||
const path = findPath(store.modelHierarchy, newId)
|
||||
if (path) {
|
||||
// 更新展开状态
|
||||
path.forEach(id => expandedKeys.value.add(id))
|
||||
|
||||
// 同步树展开状态
|
||||
path.forEach(id => {
|
||||
const node = treeRef.value.getNode(id)
|
||||
if (node) {
|
||||
node.expanded = true
|
||||
}
|
||||
})
|
||||
|
||||
// 延迟滚动,等待DOM更新
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
const element = document.getElementById('tree-node-' + newId)
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('树滚动错误:', e)
|
||||
}
|
||||
})
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// 生命周期
|
||||
// ============================================================
|
||||
|
||||
onMounted(() => {
|
||||
updateLayout()
|
||||
window.addEventListener('resize', updateLayout)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateLayout)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ============================================================
|
||||
输入框样式
|
||||
============================================================ */
|
||||
|
||||
:deep(.tech-input .el-input__wrapper) {
|
||||
background-color: rgba(15, 23, 42, 0.5);
|
||||
box-shadow: 0 0 0 1px rgba(34, 211, 238, 0.2) inset;
|
||||
}
|
||||
|
||||
:deep(.tech-input .el-input__inner) {
|
||||
color: #e2e8f0;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
:deep(.tech-input .el-input__wrapper.is-focus) {
|
||||
box-shadow: 0 0 0 1px #06b6d4 inset;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
树样式
|
||||
============================================================ */
|
||||
|
||||
.tech-tree {
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__content) {
|
||||
background-color: transparent !important;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__content:hover) {
|
||||
background-color: rgba(6, 182, 212, 0.1) !important;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node.is-current > .el-tree-node__content) {
|
||||
background-color: rgba(6, 182, 212, 0.15) !important;
|
||||
border-right: 2px solid #22d3ee;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node.is-current > .el-tree-node__content .truncate) {
|
||||
color: #22d3ee !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
滚动条样式
|
||||
============================================================ */
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(34, 211, 238, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(34, 211, 238, 0.4);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user