feat: 新增 Revit 模型查看器组件、报告日志页面及 CAD 状态管理模块。

This commit is contained in:
sladro 2026-03-02 16:34:57 +08:00
parent b53f9d37e5
commit e01376de57
3 changed files with 551 additions and 208 deletions

View File

@ -37,6 +37,14 @@
<span class="info-label">文件大小:</span>
<span class="info-value">{{ projectData.FileSizeDisplay }}</span>
</div>
<div class="info-item">
<span class="info-label">多边形面片数:</span>
<span class="info-value">{{ projectData.PolygonCount.toLocaleString() }}</span>
</div>
<div class="info-item">
<span class="info-label">特征总数:</span>
<span class="info-value">{{ projectData.FeatureCount.toLocaleString() }}</span>
</div>
<div class="info-item">
<span class="info-label">中心文件:</span>
<span class="info-value">{{ projectData.IsCentralFile ? '是' : '否' }}</span>
@ -171,9 +179,11 @@ const projectData = computed(() => {
ProjectName: project.name || 'Unknown Project',
FilePath: project.filePath || 'Unknown Path',
RevitVersion: project.revitVersion || 'Unknown Version',
FileSizeDisplay: project.fileSizeDisplay || 'Unknown Size',
FileSizeDisplay: project.fileSizeDisplay || project.fileSize || 'Unknown Size',
PolygonCount: project.polygonCount || 0,
FeatureCount: project.featureCount || 0,
IsCentralFile: project.isCentralFile || false,
IsLoaded: project.status === '活动'
IsLoaded: project.status === '已保存' || project.status === '活动'
}
})

View File

@ -25,18 +25,18 @@
</div>
<ul class="report-list">
<li v-for="log in filteredLogs" :key="log.reportId"
<li v-for="log in filteredLogs" :key="log.id || log.reportId"
class="report-item"
:class="{ active: currentReport?.reportId === log.reportId }"
:class="{ active: currentReport?.id === log.id || (currentReport?.reportId && currentReport?.reportId === log.reportId) }"
@click="selectReport(log)">
<div class="item-icon" :class="log.status">
<i :class="log.status === 'success' ? 'fas fa-check-circle' : 'fas fa-exclamation-circle'"></i>
<i :class="(log.status === 'success' || !log.status) ? 'fas fa-check-circle' : 'fas fa-exclamation-circle'"></i>
</div>
<div class="item-content">
<div class="item-title">{{ log.operation }}</div>
<div class="item-title">{{ log.operation || '系统操作' }}</div>
<div class="item-meta">
<span class="meta-id">{{ log.reportId }}</span>
<span class="meta-time">{{ log.createdAt }}</span>
<span class="meta-id">{{ log.operation_category || log.log_type || '系统日志' }}</span>
<span class="meta-time">{{ formatTime(log.timestamp || log.createdAt || log.created_at) }}</span>
</div>
</div>
</li>
@ -45,72 +45,79 @@
<!-- 右侧报告详情 -->
<div class="detail-section card-panel" v-if="currentReport">
<div class="detail-header">
<div class="report-id-badge">
<i class="fas fa-hashtag"></i>
<span>{{ currentReport.reportId }}</span>
<el-tag :type="currentReport.status === 'success' ? 'success' : 'danger'" size="small" style="margin-left: 10px">
{{ currentReport.status === 'success' ? '执行成功' : '执行异常' }}
<div class="detail-header overview-custom-header">
<div class="header-left">
<el-tag effect="dark" :type="currentReport.status === 'success' ? 'success' : (currentReport.status === 'pending' ? 'warning' : 'danger')" size="large" class="status-tag">
<i :class="currentReport.status === 'success' ? 'fas fa-check-circle' : (currentReport.status === 'pending' ? 'fas fa-spinner fa-spin' : 'fas fa-times-circle')"></i>
{{ currentReport.status === 'success' ? '执行成功' : (currentReport.status === 'pending' ? '处理中' : '执行异常') }}
</el-tag>
<span class="report-id-text" :title="currentReport.id || currentReport.reportId"><i class="fas fa-fingerprint"></i> ID: {{ currentReport.id || currentReport.reportId || 'LOG-000' }}</span>
</div>
<div class="report-time">
<i class="fas fa-clock"></i> 耗时: {{ currentReport.duration }}s
<div class="report-time" v-if="currentReport.duration">
<i class="fas fa-clock"></i> 耗时: {{ currentReport.duration }}ms
</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 class="detail-body styled-body">
<div class="overview-title-card">
<div class="overview-title-icon" :class="'icon-' + getCategoryColor(currentReport.operation_category)">
<i :class="getCategoryIconClass(currentReport.operation_category)"></i>
</div>
<h2 class="overview-title-text">{{ currentReport.operation || '系统操作' }}</h2>
</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 class="overview-info-card">
<el-descriptions class="custom-descriptions" :column="3" border>
<el-descriptions-item>
<template #label><div class="desc-label"><i class="fas fa-sitemap"></i> 所属分类</div></template>
<el-tag size="small" type="info" effect="plain">{{ currentReport.operation_category || currentReport.log_type || '系统日志' }}</el-tag>
</el-descriptions-item>
<el-descriptions-item>
<template #label><div class="desc-label"><i class="far fa-clock"></i> 执行时间</div></template>
<span class="mono">{{ formatTime(currentReport.timestamp || currentReport.createdAt || currentReport.created_at) }}</span>
</el-descriptions-item>
<el-descriptions-item>
<template #label><div class="desc-label"><i class="fas fa-desktop"></i> 操作节点</div></template>
<span class="mono">{{ currentReport.client_ip || currentReport.ip_address || '本地系统/客户端' }}</span>
</el-descriptions-item>
<el-descriptions-item v-if="currentReport.user_id || currentReport.client_id">
<template #label><div class="desc-label"><i class="fas fa-user-circle"></i> 操作人员</div></template>
{{ currentReport.user_id || currentReport.client_id }}
</el-descriptions-item>
<el-descriptions-item v-if="currentReport.level">
<template #label><div class="desc-label"><i class="fas fa-shield-alt"></i> 安全级别</div></template>
<el-tag size="small" :type="currentReport.level === 'error' ? 'danger' : 'success'">{{ currentReport.level }}</el-tag>
</el-descriptions-item>
<el-descriptions-item v-if="currentReport.action_type">
<template #label><div class="desc-label"><i class="fas fa-bolt"></i> 动作分发</div></template>
<span class="mono">{{ currentReport.action_type }}</span>
</el-descriptions-item>
</el-descriptions>
</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 class="detail-group mt-4" v-if="currentReport.details || currentReport.errorInfo || currentReport.error_message">
<h4 class="group-title"><i class="fas fa-align-left text-primary"></i> 日志详细信息 / 异常信息</h4>
<el-alert
:title="currentReport.details || currentReport.errorInfo || currentReport.error_message"
:type="currentReport.status === 'success' ? 'info' : 'error'"
:closable="false"
show-icon
class="custom-alert-box"
/>
</div>
<!-- 业务数据 -->
<div class="detail-group mt-4" v-if="hasExtraData(currentReport)">
<h4 class="group-title"><i class="fas fa-laptop-code text-warning"></i> 业务执行对象负荷 (Payload)</h4>
<div class="json-viewer-box">
<pre><code>{{ formatExtraData(currentReport) }}</code></pre>
</div>
</div>
</div>
</el-tab-pane>
@ -118,9 +125,8 @@
<div class="comparison-view">
<div class="comparison-mode-switch">
<el-radio-group v-model="comparisonMode" size="small">
<el-radio-button label="live">实时穿透 (当前CAD模型)</el-radio-button>
<el-radio-button label="auto">分析报告 (当前记录)</el-radio-button>
<el-radio-button label="manual">离线文件 (手动选择)</el-radio-button>
<el-radio-button label="live">实时穿透 (当前模型流程)</el-radio-button>
<el-radio-button label="manual">离线文件 (当前模型 vs 离线文件)</el-radio-button>
</el-radio-group>
</div>
@ -128,86 +134,59 @@
<div v-if="comparisonMode === 'live'" class="live-comparison">
<div class="live-flow-container">
<!-- 步骤 1: 捕获基准 -->
<div class="flow-step" :class="{ 'step-active': liveStep === 1, 'step-done': liveStep > 1 }">
<div class="flow-step" :class="{ 'step-active': cadStore.liveComparisonState.step === 1, 'step-done': cadStore.liveComparisonState.step > 1 }">
<div class="step-header">
<div class="step-num">1</div>
<h3>记录当前模型基准状态</h3>
</div>
<div class="step-content">
<el-button v-if="liveStep === 1" type="primary" :loading="isCapturingBaseline" @click="captureBaseline" class="live-btn">
<el-button v-if="cadStore.liveComparisonState.step === 1" type="primary" :loading="isCapturingBaseline" @click="captureBaseline" class="live-btn">
<i class="fas fa-crosshairs"></i> 一键读取 CAD 当前基准参数
</el-button>
<div v-else class="captured-data card-panel">
<h4><i class="fas fa-cube text-info"></i> 基准原始模型</h4>
<ul class="data-list">
<li><span class="lbl">模型文件:</span> <span class="val">{{ baselineData.name }}</span></li>
<li><span class="lbl">内存占用:</span> <span class="val">{{ baselineData.mem }}</span></li>
<li><span class="lbl">面片数量:</span> <span class="val">{{ baselineData.poly }}</span></li>
<li><span class="lbl">特征数量:</span> <span class="val">{{ baselineData.features }}</span></li>
<li><span class="lbl">模型文件:</span> <span class="val">{{ cadStore.liveComparisonState.baselineData?.name }}</span></li>
<li><span class="lbl">内存占用:</span> <span class="val">{{ cadStore.liveComparisonState.baselineData?.mem }}</span></li>
<li><span class="lbl">面片数量:</span> <span class="val">{{ cadStore.liveComparisonState.baselineData?.poly }}</span></li>
<li><span class="lbl">特征数量:</span> <span class="val">{{ cadStore.liveComparisonState.baselineData?.features }}</span></li>
</ul>
</div>
</div>
</div>
<!-- 步骤 2: 执行轻量化 -->
<div class="flow-step" :class="{ 'step-active': liveStep === 2, 'step-done': liveStep > 2 }">
<!-- 步骤 2: 捕获当前状态并比对 -->
<div class="flow-step" :class="{ 'step-active': cadStore.liveComparisonState.step === 2, 'step-done': cadStore.liveComparisonState.step > 2 }">
<div class="step-header">
<div class="step-num">2</div>
<h3>执行轻量化处理</h3>
<h3>捕获当前状态并生成对比</h3>
</div>
<div class="step-content">
<div v-if="liveStep <= 2" class="wait-msg">
<i class="fas fa-info-circle"></i> 记录基准后请在对应 CAD 插件中直接执行处理或由系统在此处触发
<div v-if="cadStore.liveComparisonState.step === 2" class="wait-msg">
<i class="fas fa-info-circle"></i> 记录基准后您可以前往其他模块或在 CAD 中自由执行各种操作操作完成后返回此处点击下方按钮
</div>
<el-button v-if="liveStep === 2" type="warning" :loading="isLightweighting" @click="executeLightweight" class="live-btn">
<i class="fas fa-magic"></i> 发送轻量化指令并等待
</el-button>
<div v-if="liveStep > 2" class="success-msg">
<i class="fas fa-check-circle text-success"></i> 轻量化操作已完成
</div>
</div>
</div>
<!-- 步骤 3: 捕获轻量化后状态 -->
<div class="flow-step" :class="{ 'step-active': liveStep === 3, 'step-done': liveStep > 3 }">
<div class="step-header">
<div class="step-num">3</div>
<h3>捕获结果并生成对比</h3>
</div>
<div class="step-content">
<el-button v-if="liveStep === 3" type="success" :loading="isCapturingResult" @click="captureResult" class="live-btn" plain>
<i class="fas fa-sync-alt"></i> 读取轻量化后状态并对比
<el-button v-if="cadStore.liveComparisonState.step === 2" type="success" :loading="isCapturingResult" @click="captureResult" class="live-btn" plain>
<i class="fas fa-sync-alt"></i> 读取当前最新状态并对比
</el-button>
<transition name="el-zoom-in-top">
<div v-if="liveStep === 4" class="comparison-results mt-4">
<div class="compress-rates">
<div class="rate-card">
<div class="rate-title">内存占用降低</div>
<div class="rate-value text-success">-65.4%</div>
</div>
<div class="rate-card">
<div class="rate-title">面片数量缩减</div>
<div class="rate-value text-success">-72.1%</div>
</div>
</div>
<div v-if="cadStore.liveComparisonState.step === 3" class="comparison-results mt-4">
<div class="comparison-columns mt-4">
<div class="comp-col source-col">
<h4><i class="fas fa-cube"></i> 基准状态</h4>
<ul class="prop-list">
<li><span class="prop-label">内存占用</span> <span class="prop-value">{{ baselineData.mem }}</span></li>
<li><span class="prop-label">面片数量</span> <span class="prop-value">{{ baselineData.poly }}</span></li>
<li><span class="prop-label">特征总数</span> <span class="prop-value">{{ baselineData.features }}</span></li>
<li><span class="prop-label">内存占用</span> <span class="prop-value">{{ cadStore.liveComparisonState.baselineData?.mem }}</span></li>
<li><span class="prop-label">面片数量</span> <span class="prop-value">{{ cadStore.liveComparisonState.baselineData?.poly }}</span></li>
<li><span class="prop-label">特征总数</span> <span class="prop-value">{{ cadStore.liveComparisonState.baselineData?.features }}</span></li>
</ul>
</div>
<div class="vs-divider">VS</div>
<div class="comp-col target-col">
<h4><i class="fas fa-leaf text-success"></i> 轻量化后</h4>
<h4><i class="fas fa-leaf text-success"></i> 最新状态</h4>
<ul class="prop-list">
<li><span class="prop-label">内存占用</span> <span class="prop-value text-success">{{ resultData.mem }}</span></li>
<li><span class="prop-label">面片数量</span> <span class="prop-value text-success">{{ resultData.poly }}</span></li>
<li><span class="prop-label">特征总数</span> <span class="prop-value text-success">{{ resultData.features }}</span></li>
<li><span class="prop-label">内存占用</span> <span class="prop-value text-success">{{ cadStore.liveComparisonState.resultData?.mem }}</span></li>
<li><span class="prop-label">面片数量</span> <span class="prop-value text-success">{{ cadStore.liveComparisonState.resultData?.poly }}</span></li>
<li><span class="prop-label">特征总数</span> <span class="prop-value text-success">{{ cadStore.liveComparisonState.resultData?.features }}</span></li>
</ul>
</div>
</div>
@ -222,70 +201,27 @@
</div>
</div>
<!-- 自动模式内容 -->
<div v-else-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 class="upload-box baseline-source">
<div class="status-indicator">
<i class="fas fa-desktop text-primary"></i>
</div>
<p class="source-title">当前 CAD 打开模型</p>
<p class="source-desc">将自动抓取当前插件会话中的模型参数作为基准</p>
</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>
<i class="fas fa-file-upload"></i>
<p>请选择比对目标离线文件 (.prt, .ifc, .rvt...)</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>
<el-button type="success" icon="el-icon-video-play" size="large">开始混合比对分析</el-button>
</div>
</div>
</div>
@ -303,62 +239,169 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useCADStore } from '@/stores/cad'
import creoApi from '@/services/creoApi'
import revitApi from '@/services/revitApi'
import pdmsApi from '@/services/pdmsApi'
import websocketService from '@/services/websocketService'
const searchQuery = ref('')
const activeTab = ref('overview')
const comparisonMode = ref('live')
const liveStep = ref(1) // 1: , 2: , 3: , 4:
const isCapturingBaseline = ref(false)
const isLightweighting = ref(false)
const isCapturingResult = ref(false)
const baselineData = ref(null)
const resultData = ref(null)
const captureBaseline = () => {
const cadStore = useCADStore()
const captureBaseline = async () => {
const connectedCAD = cadStore.currentCAD
if (!connectedCAD) {
ElMessage.warning('请先连接并打开对应的 CAD 插件和模型')
return
}
isCapturingBaseline.value = true
setTimeout(() => {
try {
let result;
let stats = {
mem: 'API暂未支持',
poly: 'API暂未支持',
features: 'API暂未支持'
}
if (connectedCAD.id === 'creo') {
result = await creoApi.getCurrentModel()
// /
try {
const statResult = await creoApi.getHierarchyStatistics()
if (statResult.success && statResult.data?.data?.statistics) {
let totalFeatures = 0
Object.values(statResult.data.data.statistics).forEach(v => totalFeatures += v)
if(totalFeatures > 0) stats.features = `${totalFeatures} +`
}
} catch(e) { /* 忽略层级统计失败 */ }
} else if (connectedCAD.id === 'revit') {
result = await revitApi.getCurrentModel()
} else if (connectedCAD.id === 'pdms') {
result = await pdmsApi.getCurrentModel()
}
if (result && result.success) {
const respData = result.data?.data || {}
let fileName = '当前活动模型'
if (connectedCAD.id === 'creo' && respData.fileName) {
fileName = respData.fileName
} else if (connectedCAD.id === 'revit' && respData.project?.name) {
fileName = respData.project.name
} else if (connectedCAD.id === 'pdms' && respData.ProjectInfo?.ProjectName) {
fileName = respData.ProjectInfo.ProjectName
}
//
if (respData.fileSize) stats.mem = respData.fileSize
if (respData.memoryUsage) stats.mem = respData.memoryUsage
if (respData.polygonCount) stats.poly = respData.polygonCount
if (respData.featureCount) stats.features = respData.featureCount
if (respData.componentCount) stats.features = respData.componentCount
// Revit (data.data.project.xxx)
if (connectedCAD.id === 'revit' && respData.project) {
if (respData.project.fileSizeDisplay) stats.mem = respData.project.fileSizeDisplay
if (respData.project.polygonCount !== undefined) stats.poly = respData.project.polygonCount
if (respData.project.featureCount !== undefined) stats.features = respData.project.featureCount
}
cadStore.liveComparisonState.baselineData = {
name: fileName,
mem: stats.mem,
poly: stats.poly,
features: stats.features
}
cadStore.liveComparisonState.step = 2
ElMessage.success('基准状态参数读取成功')
} else {
throw new Error(result?.error || '未能获取到开启的模型信息')
}
} catch (error) {
ElMessage.error(`获取基准数据失败: ${error.message}`)
} finally {
isCapturingBaseline.value = false
baselineData.value = {
name: 'Engine_Assembly_V8_Final.asm',
mem: '1420 MB',
poly: '2,450,120',
features: '12,054'
}
liveStep.value = 2
ElMessage.success('基准状态捕获成功')
}, 1000)
}
}
const executeLightweight = () => {
isLightweighting.value = true
setTimeout(() => {
isLightweighting.value = false
liveStep.value = 3
ElMessage.success('轻量化指令执行完成')
}, 2000)
}
const captureResult = async () => {
const connectedCAD = cadStore.currentCAD
if (!connectedCAD) {
ElMessage.warning('请先连接对应的 CAD 插件')
return
}
const captureResult = () => {
isCapturingResult.value = true
setTimeout(() => {
isCapturingResult.value = false
resultData.value = {
mem: '490 MB',
poly: '682,100',
features: '342'
try {
let result;
let stats = {
mem: 'API暂未支持',
poly: 'API暂未支持',
features: 'API暂未支持'
}
liveStep.value = 4
ElMessage.success('最新状态捕获及对比报告已生成')
}, 1000)
if (connectedCAD.id === 'creo') {
result = await creoApi.getCurrentModel()
try {
const statResult = await creoApi.getHierarchyStatistics()
if (statResult.success && statResult.data?.data?.statistics) {
let totalFeatures = 0
Object.values(statResult.data.data.statistics).forEach(v => totalFeatures += v)
if(totalFeatures > 0) stats.features = `${totalFeatures} +`
}
} catch(e) { }
} else if (connectedCAD.id === 'revit') {
result = await revitApi.getCurrentModel()
} else if (connectedCAD.id === 'pdms') {
result = await pdmsApi.getCurrentModel()
}
if (result && result.success) {
const respData = result.data?.data || {}
//
if (respData.fileSize) stats.mem = respData.fileSize
if (respData.memoryUsage) stats.mem = respData.memoryUsage
if (respData.polygonCount) stats.poly = respData.polygonCount
if (respData.featureCount) stats.features = respData.featureCount
if (respData.componentCount) stats.features = respData.componentCount
// Revit (data.data.project.xxx)
if (connectedCAD.id === 'revit' && respData.project) {
if (respData.project.fileSizeDisplay) stats.mem = respData.project.fileSizeDisplay
if (respData.project.polygonCount !== undefined) stats.poly = respData.project.polygonCount
if (respData.project.featureCount !== undefined) stats.features = respData.project.featureCount
}
cadStore.liveComparisonState.resultData = {
mem: stats.mem,
poly: stats.poly,
features: stats.features
}
cadStore.liveComparisonState.step = 3
ElMessage.success('新状态已成功捕获并生成对比报告')
} else {
throw new Error(result?.error || '未能获取到轻量化后模型信息')
}
} catch (error) {
ElMessage.error(`获取轻量化结果失败: ${error.message}`)
} finally {
isCapturingResult.value = false
}
}
const resetLiveFlow = () => {
liveStep.value = 1
baselineData.value = null
resultData.value = null
cadStore.resetLiveComparison()
}
//
@ -424,25 +467,119 @@ const mockReports = [
}
]
const logs = ref([...mockReports])
const logs = ref([...mockReports]) // 使 mock, ws
const currentReport = ref(logs.value[0])
// --- WebSocket --- //
let listeners = []
const initListeners = () => {
const onLogUpdate = (logData) => {
let newLogs = []
if (Array.isArray(logData)) {
newLogs = [...logData].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
} else if (logData.logs) {
newLogs = [...logData.logs].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
}
// mock
if (newLogs.length > 0) {
logs.value = newLogs
//
if (!currentReport.value || !logs.value.find(l => l.id === currentReport.value.id)) {
currentReport.value = logs.value[0]
}
}
}
websocketService.on('onLogUpdate', onLogUpdate)
listeners.push(['onLogUpdate', onLogUpdate])
//
websocketService.queryLogs()
}
const removeListeners = () => {
listeners.forEach(([event, callback]) => {
websocketService.off(event, callback)
})
listeners = []
}
onMounted(() => {
initListeners()
})
onUnmounted(() => {
removeListeners()
})
// --- WebSocket --- //
const filteredLogs = computed(() => {
if (!searchQuery.value) return logs.value
const q = searchQuery.value.toLowerCase()
return logs.value.filter(log => log.reportId.toLowerCase().includes(q) || log.operation.toLowerCase().includes(q))
return logs.value.filter(log =>
(log.operation && log.operation.toLowerCase().includes(q)) ||
(log.operation_category && log.operation_category.toLowerCase().includes(q))
)
})
const selectReport = (log) => {
currentReport.value = log
}
const formatTime = (timestamp) => {
if (!timestamp) return ''
try {
const date = new Date(timestamp)
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
} catch {
return timestamp
}
}
const getActionClass = (action) => {
if (!action) return ''
if (action.includes('删除')) return 'text-success'
if (action.includes('确认')) return 'text-warning'
return 'text-info'
}
const getCategoryColor = (category) => {
if (category === 'CAD操作') return 'primary'
if (category === '软件控制') return 'success'
if (category === '系统服务') return 'warning'
return 'info'
}
const getCategoryIconClass = (category) => {
if (category === 'CAD操作') return 'fas fa-cogs'
if (category === '软件控制') return 'fas fa-desktop'
if (category === '系统服务') return 'fas fa-server'
return 'fas fa-gear'
}
const hasExtraData = (log) => {
const omitKeys = ['id', 'reportId', 'log_id', 'operation', 'operation_category', 'log_type', 'timestamp', 'createdAt', 'created_at', 'status', 'duration', 'details', 'errorInfo', 'error_message', 'user_id', 'client_id', 'client_ip', 'ip_address', 'resultMetrics', 'featureDetails', 'comparisonData', 'level', 'action_type']
return Object.keys(log).some(key => !omitKeys.includes(key))
}
const formatExtraData = (log) => {
const omitKeys = ['id', 'reportId', 'log_id', 'operation', 'operation_category', 'log_type', 'timestamp', 'createdAt', 'created_at', 'status', 'duration', 'details', 'errorInfo', 'error_message', 'user_id', 'client_id', 'client_ip', 'ip_address', 'resultMetrics', 'featureDetails', 'comparisonData', 'level', 'action_type']
const extra = {}
Object.keys(log).forEach(key => {
if (!omitKeys.includes(key)) extra[key] = log[key]
})
return JSON.stringify(extra, null, 2)
}
const exportSelectedReport = () => {
if (!currentReport.value) return ElMessage.warning('没有可导出的报告')
// PDF/JSON
@ -878,6 +1015,44 @@ const exportSelectedReport = () => {
background: rgba(var(--color-success-rgb), 0.05);
}
.upload-box.baseline-source {
cursor: default;
border-style: solid;
border-color: var(--color-primary);
background: rgba(var(--color-primary-rgb), 0.02);
}
.baseline-source .status-indicator {
width: 60px;
height: 60px;
background: rgba(var(--color-primary-rgb), 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: var(--spacing-sm);
}
.baseline-source .status-indicator i {
font-size: 28px;
color: var(--color-primary);
}
.baseline-source .source-title {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--color-text-primary);
margin: 0;
}
.baseline-source .source-desc {
font-size: var(--font-size-sm);
color: var(--color-text-tertiary);
margin: 0;
text-align: center;
padding: 0 var(--spacing-lg);
}
.upload-box i {
font-size: 48px;
color: var(--color-text-tertiary);
@ -1029,4 +1204,144 @@ const exportSelectedReport = () => {
display: flex;
justify-content: center;
}
/* 新增美化:执行概览 */
.overview-custom-header {
padding: 16px 24px !important;
background: var(--color-bg-primary) !important;
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.status-tag {
font-weight: 600;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
padding: 0 12px;
height: 32px;
}
.report-id-text {
font-family: var(--font-family-mono);
color: var(--color-text-secondary);
font-size: 14px;
}
.styled-body {
max-width: 1000px;
margin: 0 auto;
}
.overview-title-card {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px dashed var(--color-border-primary);
}
.overview-title-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.overview-title-icon.icon-primary { background: rgba(var(--color-primary-rgb), 0.1); color: var(--color-primary); }
.overview-title-icon.icon-success { background: rgba(var(--color-success-rgb), 0.1); color: var(--color-success); }
.overview-title-icon.icon-warning { background: rgba(var(--color-warning-rgb), 0.1); color: var(--color-warning); }
.overview-title-icon.icon-info { background: rgba(255, 255, 255, 0.05); color: var(--color-text-secondary); }
.overview-title-text {
font-size: 24px;
font-weight: 600;
margin: 0;
color: var(--color-text-primary);
}
.overview-info-card {
border-radius: var(--size-border-radius-lg);
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
margin-bottom: var(--spacing-xl);
}
:deep(.el-descriptions__label.el-descriptions__cell.is-bordered-label) {
background-color: var(--color-bg-tertiary, #2b2b2b);
color: var(--color-text-primary);
border-color: var(--color-border-primary);
}
:deep(.el-descriptions__content.el-descriptions__cell.is-bordered-content) {
background-color: var(--color-bg-secondary, #1f1f1f);
color: var(--color-text-secondary);
border-color: var(--color-border-primary);
}
:deep(.el-descriptions) {
border: 1px solid var(--color-border-primary);
border-radius: var(--size-border-radius-lg);
overflow: hidden;
}
:deep(.el-descriptions .el-descriptions__table) {
border-radius: var(--size-border-radius-lg);
}
.desc-label {
display: flex;
align-items: center;
gap: 6px;
font-weight: 500;
color: var(--color-text-primary);
}
.mono {
font-family: var(--font-family-mono);
color: var(--color-text-regular);
}
.custom-alert-box {
border-radius: var(--size-border-radius-lg);
white-space: pre-wrap;
word-break: break-all;
padding: 16px;
align-items: flex-start;
background-color: rgba(var(--color-primary-rgb), 0.1);
border: 1px solid rgba(var(--color-primary-rgb), 0.2);
}
:deep(.custom-alert-box .el-alert__title) {
color: var(--color-text-primary);
font-size: 14px;
}
.json-viewer-box {
background: #1e1e1e;
border-radius: var(--size-border-radius-lg);
padding: 16px;
overflow-x: auto;
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.2);
}
.json-viewer-box pre {
margin: 0;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
line-height: 1.5;
color: #9cdcfe;
}
</style>

View File

@ -97,6 +97,22 @@ export const useCADStore = defineStore('cad', () => {
draggedFile.value = file
}
// 实时穿透对比状态 (全局保留)
const liveComparisonState = ref({
step: 1, // 1: 准备抓取基准, 2: 已抓取基准(等待抓取结果), 3: 完成对比
baselineData: null,
resultData: null
})
// 重置实时穿透对比状态
const resetLiveComparison = () => {
liveComparisonState.value = {
step: 1,
baselineData: null,
resultData: null
}
}
return {
// 状态
cadConnections,
@ -111,6 +127,8 @@ export const useCADStore = defineStore('cad', () => {
setCADConnection,
setCADConnecting,
setCurrentProjectName,
setDraggedFile
setDraggedFile,
liveComparisonState,
resetLiveComparison
}
})