feat: add ReportLogViewer and UniversalConverter components for displaying analysis reports and handling universal conversions.

This commit is contained in:
sladro 2026-02-28 14:14:13 +08:00
parent 0073eb3a6f
commit 090cf8b3cb
2 changed files with 463 additions and 63 deletions

View File

@ -58,58 +58,140 @@
</div>
</div>
<div class="detail-body">
<!-- 统计摘要 -->
<div class="stat-summary">
<div class="summary-box">
<span class="box-label">分析模型数</span>
<span class="box-value accent">{{ currentReport.resultMetrics.modelCount }}</span>
</div>
<div class="summary-box">
<span class="box-label">识别特征总数</span>
<span class="box-value accent">{{ currentReport.resultMetrics.featureCount }}</span>
</div>
<div class="summary-box">
<span class="box-label">处理部件数</span>
<span class="box-value accent">{{ currentReport.resultMetrics.processedCount }}</span>
</div>
</div>
<el-tabs v-model="activeTab" class="report-tabs">
<el-tab-pane label="执行概览" name="overview">
<div class="detail-body">
<!-- 统计摘要 -->
<div class="stat-summary">
<div class="summary-box">
<span class="box-label">分析模型数</span>
<span class="box-value accent">{{ currentReport.resultMetrics.modelCount }}</span>
</div>
<div class="summary-box">
<span class="box-label">识别特征总数</span>
<span class="box-value accent">{{ currentReport.resultMetrics.featureCount }}</span>
</div>
<div class="summary-box">
<span class="box-label">处理部件数</span>
<span class="box-value accent">{{ currentReport.resultMetrics.processedCount }}</span>
</div>
</div>
<!-- 特征详情表格 -->
<div class="detail-group">
<h4 class="group-title"><i class="fas fa-cube"></i> 特征结果详情</h4>
<el-table :data="currentReport.featureDetails" style="width: 100%" max-height="300" border>
<el-table-column prop="featureName" label="特征名称" width="150" />
<el-table-column prop="featureType" label="类型" width="120">
<template #default="scope">
<el-tag size="small">{{ scope.row.featureType }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="recommendedAction" label="建议动作" width="120">
<template #default="scope">
<span :class="getActionClass(scope.row.recommendedAction)">
{{ scope.row.recommendedAction }}
</span>
</template>
</el-table-column>
<el-table-column label="后续步骤">
<template #default="scope">
<div v-for="(step, idx) in scope.row.recommendedSteps" :key="idx" class="step-text">
{{ idx + 1 }}. {{ step }}
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 特征详情表格 -->
<div class="detail-group">
<h4 class="group-title"><i class="fas fa-cube"></i> 特征结果详情</h4>
<el-table :data="currentReport.featureDetails" style="width: 100%" border>
<el-table-column prop="featureName" label="特征名称" width="150" />
<el-table-column prop="featureType" label="类型" width="120">
<template #default="scope">
<el-tag size="small">{{ scope.row.featureType }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="recommendedAction" label="建议动作" width="120">
<template #default="scope">
<span :class="getActionClass(scope.row.recommendedAction)">
{{ scope.row.recommendedAction }}
</span>
</template>
</el-table-column>
<el-table-column label="后续步骤">
<template #default="scope">
<div v-for="(step, idx) in scope.row.recommendedSteps" :key="idx" class="step-text">
{{ idx + 1 }}. {{ step }}
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 异常信息区 -->
<div class="detail-group error-group" v-if="currentReport.errorInfo">
<h4 class="group-title error-text"><i class="fas fa-exclamation-triangle"></i> 异常日志</h4>
<div class="error-console">
{{ currentReport.errorInfo }}
</div>
</div>
</div>
<!-- 异常信息区 -->
<div class="detail-group error-group" v-if="currentReport.errorInfo">
<h4 class="group-title error-text"><i class="fas fa-exclamation-triangle"></i> 异常日志</h4>
<div class="error-console">
{{ currentReport.errorInfo }}
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="模型对比" name="comparison">
<div class="comparison-view">
<div class="comparison-mode-switch">
<el-radio-group v-model="comparisonMode" size="small">
<el-radio-button label="auto">自动读取 (当前任务记录)</el-radio-button>
<el-radio-button label="manual">手动选择比对文件</el-radio-button>
</el-radio-group>
</div>
<!-- 自动模式内容 -->
<div v-if="comparisonMode === 'auto'" class="auto-comparison">
<div v-if="!currentReport.comparisonData" class="empty-data">
<i class="fas fa-info-circle"></i> 当前报告未包含比对数据
</div>
<div v-else class="comparison-dashboard">
<div class="result-badges">
<el-tag effect="dark" type="info">{{ currentReport.comparisonData.sourceFormat }}</el-tag>
<i class="fas fa-arrow-right badge-arrow"></i>
<el-tag effect="dark" type="success">{{ currentReport.comparisonData.targetFormat }}</el-tag>
</div>
<div class="compress-rates">
<div class="rate-card">
<div class="rate-title">文件体积减小</div>
<div class="rate-value text-success">{{ currentReport.comparisonData.sizeReduction }}</div>
</div>
<div class="rate-card">
<div class="rate-title">多边形面片缩减</div>
<div class="rate-value text-success">{{ currentReport.comparisonData.polygonReduction }}</div>
</div>
</div>
<div class="comparison-columns">
<div class="comp-col source-col">
<h4><i class="fas fa-cube"></i> 基准原始模型</h4>
<ul class="prop-list">
<li v-for="(val, key) in currentReport.comparisonData.sourceProps" :key="key">
<span class="prop-label">{{ key }}</span>
<span class="prop-value">{{ val }}</span>
</li>
</ul>
</div>
<div class="vs-divider">VS</div>
<div class="comp-col target-col">
<h4><i class="fas fa-leaf"></i> 轻量化结果模型</h4>
<ul class="prop-list">
<li v-for="(val, key) in currentReport.comparisonData.targetProps" :key="key">
<span class="prop-label">{{ key }}</span>
<span class="prop-value text-success">{{ val }}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 手动模式内容 -->
<div v-else class="manual-comparison">
<div class="upload-panels">
<div class="upload-box">
<i class="fas fa-cloud-upload-alt"></i>
<p>请选择基准源文件 (.asm, .rvt...)</p>
<el-button size="small">选择基准文件</el-button>
</div>
<div class="vs-icon">VS</div>
<div class="upload-box target">
<i class="fas fa-cloud-upload-alt"></i>
<p>请选择轻量化目标 (.prt, .ifc...)</p>
<el-button size="small" type="primary">选择目标文件</el-button>
</div>
</div>
<div class="manual-actions">
<el-button type="success" icon="el-icon-video-play" size="large">开始比对分析</el-button>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<div class="detail-section empty-state card-panel" v-else>
@ -126,6 +208,8 @@ import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
const searchQuery = ref('')
const activeTab = ref('overview')
const comparisonMode = ref('auto')
//
const mockReports = [
@ -142,6 +226,26 @@ const mockReports = [
{ featureName: 'FILLET_002', featureType: '倒角', recommendedAction: '建议保留', recommendedSteps: ['标记为特征', '跳过删除向导'] },
{ featureName: 'POCKET_003', featureType: '凹槽', recommendedAction: '用户确认', recommendedSteps: ['提示用户审查该凸台'] }
],
comparisonData: {
sourceFormat: 'ASM (Creo)',
targetFormat: 'Shrinkwrap PRT',
sizeReduction: '76.5%',
polygonReduction: '45.2%',
sourceProps: {
'文件大小': '125.4 MB',
'多边形体积': '1,245,600 面片',
'特征/部件总数': '4,520 个',
'装配体层级': '12 层级',
'外包络体积': '15.42 m³'
},
targetProps: {
'文件大小': '29.5 MB',
'多边形体积': '682,100 面片',
'特征/部件总数': '342 个 (不可见件全清除)',
'装配体层级': '单层 (已重组成一个实体)',
'外包络体积': '15.41 m³ (误差 <0.1%)'
}
},
errorInfo: null
},
{
@ -354,8 +458,6 @@ const exportSelectedReport = () => {
.detail-body {
padding: var(--spacing-lg);
overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
gap: var(--spacing-xl);
@ -444,4 +546,207 @@ const exportSelectedReport = () => {
--el-table-text-color: var(--color-text-primary);
--el-table-header-text-color: var(--color-text-primary);
}
/* 对比视图样式 */
.report-tabs {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
:deep(.el-tabs__content) {
flex: 1;
overflow-y: auto;
padding: 0;
}
:deep(.el-tabs__header) {
margin-bottom: 0;
padding: 0 var(--spacing-lg);
background: var(--color-bg-primary);
border-bottom: 1px solid var(--color-border-primary);
}
.comparison-view {
padding: var(--spacing-lg);
display: flex;
flex-direction: column;
gap: var(--spacing-xl);
}
.comparison-mode-switch {
display: flex;
justify-content: center;
margin-bottom: var(--spacing-md);
}
.result-badges {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
font-size: var(--font-size-lg);
}
.badge-arrow {
color: var(--color-text-secondary);
}
.compress-rates {
display: flex;
gap: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
}
.rate-card {
flex: 1;
background: var(--color-white-rgb-05);
border-radius: var(--size-border-radius);
padding: var(--spacing-lg);
text-align: center;
border: 1px solid var(--color-border-primary);
}
.rate-title {
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
margin-bottom: var(--spacing-sm);
}
.rate-value {
font-size: 32px;
font-weight: bold;
}
.comparison-columns {
display: flex;
gap: var(--spacing-lg);
align-items: stretch;
}
.comp-col {
flex: 1;
background: var(--color-bg-primary);
border: 1px dashed var(--color-border-primary);
border-radius: var(--size-border-radius);
padding: var(--spacing-lg);
}
.comp-col h4 {
margin: 0 0 var(--spacing-lg) 0;
display: flex;
align-items: center;
gap: var(--spacing-sm);
color: var(--color-text-primary);
border-bottom: 1px solid var(--color-border-primary);
padding-bottom: var(--spacing-sm);
}
.vs-divider {
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 24px;
color: var(--color-text-tertiary);
padding: 0 var(--spacing-md);
}
.prop-list {
list-style: none;
padding: 0;
margin: 0;
}
.prop-list li {
display: flex;
justify-content: space-between;
padding: var(--spacing-sm) 0;
border-bottom: 1px solid var(--color-white-rgb-05);
}
.prop-list li:last-child {
border-bottom: none;
}
.prop-label {
color: var(--color-text-secondary);
}
.prop-value {
font-weight: 500;
text-align: right;
}
.empty-data {
text-align: center;
padding: 40px;
color: var(--color-text-tertiary);
font-size: var(--font-size-lg);
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
}
.empty-data i {
font-size: 48px;
}
.upload-panels {
display: flex;
gap: var(--spacing-xl);
align-items: center;
margin-bottom: var(--spacing-xl);
}
.upload-box {
flex: 1;
height: 200px;
border: 2px dashed var(--color-border-primary);
border-radius: var(--size-border-radius-lg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--spacing-md);
color: var(--color-text-secondary);
transition: all 0.3s;
cursor: pointer;
background: var(--color-bg-primary);
}
.upload-box:hover {
border-color: var(--color-primary);
background: rgba(var(--color-primary-rgb), 0.05);
}
.upload-box.target:hover {
border-color: var(--color-success);
background: rgba(var(--color-success-rgb), 0.05);
}
.upload-box i {
font-size: 48px;
color: var(--color-text-tertiary);
}
.upload-box:hover i {
color: var(--color-primary);
}
.upload-box.target:hover i {
color: var(--color-success);
}
.vs-icon {
font-size: 32px;
font-weight: bold;
color: var(--color-text-tertiary);
}
.manual-actions {
text-align: center;
}
</style>

View File

@ -7,19 +7,41 @@
</div>
<div class="header-text">
<h2>格式转换</h2>
<p>将STP文件转换为GLTF格式用于Web展示</p>
<p v-if="currentMode === 'stp-to-gltf'">将STP文件转换为GLTF格式用于Web展示</p>
<p v-else>将IFC建筑信息模型转换为STP通用三维格式</p>
</div>
</div>
</div>
<div class="converter-content">
<div class="converter-tabs">
<div
class="tab-item"
:class="{ active: currentMode === 'stp-to-gltf' }"
@click="currentMode = 'stp-to-gltf'"
>
<i class="fas fa-cube"></i>
<span>STP GLTF</span>
</div>
<div
class="tab-item"
:class="{ active: currentMode === 'ifc-to-stp' }"
@click="currentMode = 'ifc-to-stp'"
>
<i class="fas fa-building"></i>
<span>IFC STP</span>
</div>
</div>
<div class="conversion-flow">
<div class="flow-step">
<div class="step-icon">
<i class="fas fa-file-upload"></i>
</div>
<h3>1. 选择STP文件</h3>
<p>选择要转换的STEP格式文件</p>
<h3 v-if="currentMode === 'stp-to-gltf'">1. 选择STP文件</h3>
<h3 v-else>1. 选择IFC文件</h3>
<p v-if="currentMode === 'stp-to-gltf'">选择要转换的STEP格式文件</p>
<p v-else>选择要转换的IFC格式文件</p>
</div>
<div class="flow-arrow">
<i class="fas fa-arrow-right"></i>
@ -29,7 +51,8 @@
<i class="fas fa-cogs"></i>
</div>
<h3>2. 转换处理</h3>
<p>自动处理几何数据和材质</p>
<p v-if="currentMode === 'stp-to-gltf'">自动处理几何数据和材质</p>
<p v-else>解析BIM数据并重建BREP几何</p>
</div>
<div class="flow-arrow">
<i class="fas fa-arrow-right"></i>
@ -38,18 +61,22 @@
<div class="step-icon">
<i class="fas fa-download"></i>
</div>
<h3>3. 下载GLTF</h3>
<p>获取Web优化的3D模型文件</p>
<h3 v-if="currentMode === 'stp-to-gltf'">3. 下载GLTF</h3>
<h3 v-else>3. 下载STP</h3>
<p v-if="currentMode === 'stp-to-gltf'">获取Web优化的3D模型文件</p>
<p v-else>获取通用的STEP实体模型文件</p>
</div>
</div>
<div class="converter-panel">
<div class="upload-section">
<div class="upload-area" id="stp-upload-area">
<div class="upload-area" id="file-upload-area">
<i class="fas fa-cloud-upload-alt"></i>
<h4>拖拽STP文件到此处</h4>
<h4 v-if="currentMode === 'stp-to-gltf'">拖拽STP文件到此处</h4>
<h4 v-else>拖拽IFC文件到此处</h4>
<p>或点击选择文件</p>
<input type="file" id="stp-file-input" accept=".stp,.step" class="hidden-input">
<input v-if="currentMode === 'stp-to-gltf'" type="file" id="stp-file-input" accept=".stp,.step" class="hidden-input">
<input v-else type="file" id="ifc-file-input" accept=".ifc" class="hidden-input">
<button class="upload-btn" @click="selectFile">
<i class="fas fa-folder-open"></i>
选择文件
@ -59,7 +86,8 @@
<div class="conversion-options">
<h4>转换选项</h4>
<div class="options-grid">
<!-- STP to GLTF Options -->
<div class="options-grid" v-if="currentMode === 'stp-to-gltf'">
<div class="option-item">
<label>输出质量:</label>
<select id="gltf-quality">
@ -87,6 +115,36 @@
</label>
</div>
</div>
<!-- IFC to STP Options -->
<div class="options-grid" v-else>
<div class="option-item">
<label>STEP版本:</label>
<select id="stp-version">
<option value="AP203">AP203</option>
<option value="AP214" selected>AP214</option>
<option value="AP242">AP242</option>
</select>
</div>
<div class="option-item">
<label>
<input type="checkbox" id="ifc-heal" checked>
自动修复几何缺陷
</label>
</div>
<div class="option-item">
<label>
<input type="checkbox" id="ifc-colors" checked>
导出颜色/材质信息
</label>
</div>
<div class="option-item">
<label>
<input type="checkbox" id="ifc-props">
包含属性数据
</label>
</div>
</div>
</div>
<div class="conversion-actions">
@ -118,8 +176,10 @@
</template>
<script setup>
// - UI
import { ref } from 'vue'
// - UI
const currentMode = ref('stp-to-gltf')
const selectFile = () => {
// Vue
@ -176,6 +236,41 @@ const selectFile = () => {
padding: 32px;
}
.converter-tabs {
display: flex;
gap: 16px;
margin-bottom: 32px;
padding: 8px;
background: var(--color-bg-tertiary);
border-radius: 12px;
border: 1px solid var(--color-border-primary);
}
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 24px;
border-radius: 8px;
color: var(--color-text-secondary);
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.tab-item:hover {
background: var(--color-bg-hover);
color: var(--color-text-primary);
}
.tab-item.active {
background: var(--color-primary-gradient);
color: white;
box-shadow: 0 4px 12px var(--color-primary-rgb);
}
.conversion-flow {
display: flex;
align-items: center;