feat: 实现层级分析动态树形展开和子组件加载功能

主要功能:
• 实现层级分析初始请求使用target_level=0获取顶层模型
• 新增getChildrenComponents API方法支持动态加载子组件
• 重构层级分析结果页面使用响应式数组直接管理组件数据
• 实现点击展开按钮动态加载并插入子组件到正确位置
• 修复子组件层级计算逻辑,确保父组件level+1
• 修复组件选择使用路径作为唯一标识,避免多选问题
• 优化界面显示,移除文件大小列,调整总组件数显示
• 添加componentChildren端点配置支持子组件查询

技术改进:
• 从computed改为ref数组提升响应性能
• 使用component.path作为唯一标识解决ID重复问题
• 简化树形视图逻辑,移除复杂的展开状态管理
• 优化API参数处理,支持可选componentId参数

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sladro 2025-09-21 13:15:09 +08:00
parent 8bbbcb5383
commit 3c2aab553b
3 changed files with 123 additions and 70 deletions

View File

@ -49,7 +49,7 @@
<i class="fas fa-cubes"></i>
</div>
<div class="card-content">
<div class="card-value">{{ hierarchyData.length }}</div>
<div class="card-value">{{ (analysisResults?.total_components || 0) - 1 }}</div>
<div class="card-label">总组件数</div>
</div>
</div>
@ -74,8 +74,8 @@
</div>
<div class="tree-container">
<div
v-for="component in sortedHierarchyData"
:key="component.id"
v-for="(component, index) in hierarchyData"
:key="index"
v-show="shouldShowNode(component)"
class="tree-node"
:class="`safety-${component.deletion_safety}`"
@ -84,11 +84,11 @@
<div
class="node-toggle"
:class="{
expanded: expandedNodes.has(component.id),
'has-children': hasChildren(component.id)
expanded: component.expanded,
'has-children': hasChildren(component)
}"
@click="toggleNode(component)"
v-if="hasChildren(component.id)"
@click="toggleNode(component, index)"
v-if="hasChildren(component)"
>
<i class="fas fa-chevron-right"></i>
</div>
@ -97,7 +97,7 @@
<i class="fas fa-cube"></i>
</div>
<div class="node-content">
<div class="node-name">{{ component.id }}</div>
<div class="node-name">{{ component.filename }}</div>
<div class="node-info">
<span class="node-type">{{ component.type }}</span>
<span class="node-level">层级: {{ component.level }}</span>
@ -105,8 +105,7 @@
</div>
</div>
<div class="node-actions">
<span class="node-size">{{ component.file_size }}</span>
<button class="node-select-btn" :class="{ selected: selectedComponents.has(component.id) }" @click="toggleSelection(component.id)">
<button class="node-select-btn" :class="{ selected: selectedComponents.has(component.path) }" @click="toggleSelection(component.path)">
<i class="fas fa-check"></i>
</button>
</div>
@ -129,19 +128,18 @@
<th>类型</th>
<th>层级</th>
<th>路径</th>
<th>文件大小</th>
<th>删除安全性</th>
</tr>
</thead>
<tbody>
<tr v-for="component in hierarchyData" :key="component.id" class="component-row" :class="`safety-${component.deletion_safety}`">
<tr v-for="(component, index) in hierarchyData" :key="index" class="component-row" :class="`safety-${component.deletion_safety}`">
<td>
<input type="checkbox" :checked="selectedComponents.has(component.id)" @change="toggleSelection(component.id)">
<input type="checkbox" :checked="selectedComponents.has(component.path)" @change="toggleSelection(component.path)">
</td>
<td>
<div class="component-name">
<i class="fas fa-cube"></i>
{{ component.name }}
{{ component.filename }}
</div>
</td>
<td>{{ component.type }}</td>
@ -149,7 +147,6 @@
<td>
<div class="component-path">{{ component.path }}</div>
</td>
<td>{{ component.file_size }}</td>
<td>
<span class="safety-badge" :class="`safety-${component.deletion_safety}`">
{{ getSafetyLabel(component.deletion_safety) }}
@ -190,7 +187,7 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, onMounted, watch } from 'vue'
import { creoApi } from '@/services/creoApi'
import { ElMessageBox } from 'element-plus'
@ -205,7 +202,7 @@ const emit = defineEmits(['show-hierarchy-deletion-params'])
const currentView = ref('tree')
const selectedComponents = ref(new Set())
const expandedNodes = ref(new Set())
const hierarchyData = ref([]) //
// 使APIShellAnalysisResult
const analysisResults = computed(() => {
@ -215,8 +212,8 @@ const analysisResults = computed(() => {
return props.analysisData.data
})
// API
const hierarchyData = computed(() => {
//
const initializeHierarchyData = () => {
const allData = []
if (analysisResults.value) {
@ -229,7 +226,8 @@ const hierarchyData = computed(() => {
level.components.forEach(component => {
allData.push({
...component,
deletion_safety: component.deletion_safety || 'safe'
deletion_safety: component.deletion_safety || 'safe',
expanded: false //
})
})
}
@ -243,8 +241,12 @@ const hierarchyData = computed(() => {
if (recommendations.safe_deletions && Array.isArray(recommendations.safe_deletions)) {
recommendations.safe_deletions.forEach(comp => {
//
if (!allData.find(existing => existing.id === comp.id)) {
allData.push({ ...comp, deletion_safety: 'safe' })
if (!allData.find(existing => existing.path === comp.path && existing.id === comp.id)) {
allData.push({
...comp,
deletion_safety: 'safe',
expanded: false
})
}
})
}
@ -252,16 +254,20 @@ const hierarchyData = computed(() => {
if (recommendations.risky_deletions && Array.isArray(recommendations.risky_deletions)) {
recommendations.risky_deletions.forEach(comp => {
//
if (!allData.find(existing => existing.id === comp.id)) {
allData.push({ ...comp, deletion_safety: 'risky' })
if (!allData.find(existing => existing.path === comp.path && existing.id === comp.id)) {
allData.push({
...comp,
deletion_safety: 'risky',
expanded: false
})
}
})
}
}
}
return allData
})
hierarchyData.value = allData
}
@ -292,59 +298,66 @@ const getParentId = (component) => {
return null
}
//
const sortedHierarchyData = computed(() => {
return [...hierarchyData.value].sort((a, b) => {
//
const pathA = a.path || a.id || ''
const pathB = b.path || b.id || ''
return pathA.localeCompare(pathB)
})
})
//
//
const hasChildren = (nodeId) => {
return hierarchyData.value.some(comp => getParentId(comp) === nodeId)
const hasChildren = (component) => {
return component.children_count > 0
}
//
//
const shouldShowNode = (component) => {
//
if (component.level === 0) return true
//
let current = component
while (current && current.level > 0) {
const parentId = getParentId(current)
if (!parentId) return false //
//
if (!expandedNodes.value.has(parentId)) return false
//
current = hierarchyData.value.find(c => c.id === parentId)
}
return true
}
const toggleNode = (component) => {
const nodeId = component.id
if (expandedNodes.value.has(nodeId)) {
//
expandedNodes.value.delete(nodeId)
const toggleNode = async (component, index) => {
if (component.expanded) {
//
component.expanded = false
//
let removeCount = 0
for (let i = index + 1; i < hierarchyData.value.length; i++) {
const nextComponent = hierarchyData.value[i]
if (nextComponent.level > component.level) {
removeCount++
} else {
break
}
}
if (removeCount > 0) {
hierarchyData.value.splice(index + 1, removeCount)
}
} else {
//
expandedNodes.value.add(nodeId)
//
if (component.children_count > 0) {
try {
const componentId = component.level === 0 ? null : component.id
const result = await creoApi.getChildrenComponents(component.path, componentId)
if (result.success && result.data.data.children) {
//
const processedChildren = result.data.data.children.map(child => ({
...child,
level: component.level + 1, // 使level + 1
parent_id: component.id,
deletion_safety: child.deletion_safety || 'safe',
expanded: false
}))
//
hierarchyData.value.splice(index + 1, 0, ...processedChildren)
component.expanded = true
}
} catch (error) {
console.error('加载子组件失败:', error)
}
}
}
}
const toggleSelection = (componentId) => {
if (selectedComponents.value.has(componentId)) {
selectedComponents.value.delete(componentId)
const toggleSelection = (componentPath) => {
if (selectedComponents.value.has(componentPath)) {
selectedComponents.value.delete(componentPath)
} else {
selectedComponents.value.add(componentId)
selectedComponents.value.add(componentPath)
}
}
@ -384,10 +397,8 @@ const deleteSelectedComponents = async () => {
}
)
//
// selectedComponents
const componentPaths = Array.from(selectedComponents.value)
.map(id => hierarchyData.value.find(c => c.id === id)?.path)
.filter(Boolean)
// APIapiClient
const result = await creoApi.deleteComponentsByPath(componentPaths)
@ -396,6 +407,16 @@ const deleteSelectedComponents = async () => {
selectedComponents.value.clear()
}
}
//
onMounted(() => {
initializeHierarchyData()
})
// props
watch(() => props.analysisData, () => {
initializeHierarchyData()
}, { deep: true })
</script>
<style scoped>

View File

@ -89,6 +89,7 @@ const CAD_SOFTWARE_DEFINITIONS = {
shellAnalysis: '/api/analysis/shell-analysis',
optimization: '/api/creo/shrinkwrap/shell',
deleteComponent: '/api/creo/component/delete-by-path',
componentChildren: '/api/creo/component/children',
export: '/api/export/model'
}
},

View File

@ -99,7 +99,8 @@ class CreoApiService {
"software_type": SOFTWARE_TYPE,
"project_name": "overall_top_design.asm",
"max_depth": DEFAULT_MAX_RESULTS,
"include_geometry": true
"include_geometry": true,
"target_level": 0
}, {
operationContext: {
software: 'Creo',
@ -241,6 +242,36 @@ class CreoApiService {
}
})
}
/**
* 获取指定组件的子组件
* @param {string} componentPath - 组件路径
* @param {number} componentId - 组件ID (Creo Feature ID)
* @returns {Promise<{success: boolean, data?: any, error?: string}>}
*/
async getChildrenComponents(componentPath, componentId = null) {
if (!componentPath) {
throw new Error('组件路径不能为空')
}
const url = buildApiUrl(this.softwareName, 'componentChildren')
const requestData = {
"component_path": componentPath
}
// 只有当componentId存在时才添加到请求中
if (componentId) {
requestData.component_id = componentId
}
return await apiClient.post(url, requestData, {
operationContext: {
software: 'Creo',
operation: '获取子组件'
}
})
}
}
// 导出单例实例