feat: 添加Revit模型轻量化处理功能,包括专用界面、API服务和分析仪表盘。

This commit is contained in:
sladro 2026-02-23 17:10:27 +08:00
parent fd4c9fefa1
commit 29904d4479
7 changed files with 764 additions and 14 deletions

6
.agents/rules/rule.md Normal file
View File

@ -0,0 +1,6 @@
---
trigger: always_on
---
这个项目通过不同的cad插件来管理不同的cad软件。这个项目属于前端项目来连接不同的cad插件
项目如果接入新的接口要注意1接口的标准接入流程是什么按照项目现有的接入流程来2接口是针对哪个cad插件来的不要搞错

View File

@ -57,23 +57,24 @@
</div>
<div
class="revit-strategy-card disabled"
data-strategy="custom"
class="revit-strategy-card"
data-strategy="envelope-optimization"
@click="openEnvelopeOptimization"
>
<div class="strategy-icon">
<i class="fas fa-sliders-h"></i>
<i class="fas fa-compress-arrows-alt"></i>
</div>
<h5>自定义分析</h5>
<h5>模型轻量化处理</h5>
<p>
根据项目特点自定义分析参数手动选择需要优化的建筑构件类型和优化级别
对Revit模型进行薄壳轻量化保留主要外部轮廓删除多余内部模型显著减少文件体积
</p>
<div class="features">
<span>参数调节</span>
<span>构件选择</span>
<span>精细控制</span>
<span>专业定制</span>
<span>保留轮廓</span>
<span>删除内部</span>
<span>建筑专用</span>
<span>模型减重</span>
</div>
<div class="status-badge">开发中</div>
<div class="status-badge" style="background: var(--color-success)">新功能</div>
</div>
</div>
@ -235,6 +236,10 @@ const openProject = () => {
emit('page-change', 'connection')
}
const openEnvelopeOptimization = () => {
emit('page-change', 'revit-envelope-optimization')
}
const refreshConnection = () => {
ElNotification({
title: '刷新连接',

View File

@ -0,0 +1,681 @@
<template>
<div class="revit-envelope-optimization revit-theme">
<div class="header-actions">
<button class="icon-btn" @click="goBack" title="返回">
<i class="fas fa-arrow-left"></i> 返回分析中心
</button>
</div>
<div class="optimization-header">
<h2>
<i class="fas fa-compress-arrows-alt"></i>
Revit模型轻量化处理
</h2>
<p class="subtitle">保留建筑模型的外部轮廓自动删除内部构件显著减少文件体积并加速渲染</p>
</div>
<!-- 任务参数设置区 -->
<div class="settings-panel" v-if="taskStatus === 'idle' || taskStatus === 'failed' || taskStatus === 'cancelled'">
<div class="setting-group">
<label>处理模式 (Mode)</label>
<div class="mode-options">
<label class="mode-card" :class="{ active: formData.mode === 'Conservative' }">
<input type="radio" v-model="formData.mode" value="Conservative" />
<div class="mode-content">
<h5>保守 保留</h5>
<p>最大程度保留构件</p>
</div>
</label>
<label class="mode-card" :class="{ active: formData.mode === 'Standard' }">
<input type="radio" v-model="formData.mode" value="Standard" />
<div class="mode-content">
<h5>标准 (推荐)</h5>
<p>平衡显示效果与体积</p>
</div>
</label>
<label class="mode-card" :class="{ active: formData.mode === 'Aggressive' }">
<input type="radio" v-model="formData.mode" value="Aggressive" />
<div class="mode-content">
<h5>激进 删除</h5>
<p>尽可能多地移除内部细节</p>
</div>
</label>
<label class="mode-card" :class="{ active: formData.mode === 'EnvelopeOnly' }">
<input type="radio" v-model="formData.mode" value="EnvelopeOnly" />
<div class="mode-content">
<h5>仅保留轮廓</h5>
<p>严格只保留最外层表皮</p>
</div>
</label>
</div>
</div>
<div class="setting-group toggle-group">
<label>安全设置</label>
<div class="toggle-control">
<span>操作前备份原始文件</span>
<el-switch v-model="formData.backupOriginal" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
</div>
</div>
<div class="action-bar">
<button class="primary-btn" @click="startTask" :disabled="isStarting">
<i class="fas" :class="isStarting ? 'fa-spinner fa-spin' : 'fa-play'"></i>
{{ isStarting ? '正在初始化...' : '开始执行轻量化' }}
</button>
</div>
<div v-if="errorMessage" class="error-message">
<i class="fas fa-exclamation-triangle"></i> {{ errorMessage }}
</div>
</div>
<!-- 任务执行中进度区 -->
<div class="execution-panel" v-else-if="taskStatus === 'Running' || taskStatus === 'Pending'">
<h3>
<i class="fas fa-cog fa-spin"></i>
正在处理模型请耐心等待...
</h3>
<p class="status-desc">当前状态: {{ taskStatus === 'Pending' ? '排队中' : '运行中' }}</p>
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill active"></div>
</div>
</div>
<div class="execution-actions">
<button class="cancel-btn" @click="cancelTask">
<i class="fas fa-times"></i> 取消任务
</button>
</div>
</div>
<!-- 任务结果区 -->
<div class="result-panel" v-else-if="taskStatus === 'Completed' && taskResult">
<div class="success-header">
<i class="fas fa-check-circle success-icon"></i>
<h3>模型轻量化完成</h3>
</div>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-icon"><i class="fas fa-trash-alt"></i></div>
<div class="metric-info">
<div class="metric-value">{{ taskResult.removedCount }}</div>
<div class="metric-label">移除内部构件数</div>
</div>
</div>
<div class="metric-card">
<div class="metric-icon"><i class="fas fa-compress"></i></div>
<div class="metric-info">
<div class="metric-value highlight">{{ taskResult.reduction }}</div>
<div class="metric-label">体积减小比例</div>
</div>
</div>
<div class="metric-card">
<div class="metric-icon"><i class="fas fa-stopwatch"></i></div>
<div class="metric-info">
<div class="metric-value">{{ taskResult.processingTimeSeconds }}s</div>
<div class="metric-label">处理耗时</div>
</div>
</div>
</div>
<div class="details-box">
<h4>文件大小对比</h4>
<div class="size-comparison">
<div class="size-item original">
<span>原始大小</span>
<strong>{{ taskResult.originalSize }}</strong>
</div>
<div class="size-arrow">
<i class="fas fa-long-arrow-alt-right"></i>
</div>
<div class="size-item optimized">
<span>优化后大小</span>
<strong>{{ taskResult.optimizedSize }}</strong>
</div>
</div>
<div v-if="taskResult.backupPath" class="backup-info">
<i class="fas fa-info-circle"></i>
原文件已备份至: <span>{{ taskResult.backupPath }}</span>
</div>
</div>
<div class="result-actions">
<button class="primary-btn" @click="resetTask">
<i class="fas fa-redo"></i> 再次处理其他参数
</button>
<button class="secondary-btn" @click="goBack">
<i class="fas fa-home"></i> 返回分析中心
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onUnmounted } from 'vue'
import { ElNotification, ElSwitch } from 'element-plus'
import revitApi from '@/services/revitApi'
const emit = defineEmits(['page-change'])
//
// idle | Pending | Running | Completed | Failed | Cancelled
const taskStatus = ref('idle')
const currentTaskId = ref(null)
const isStarting = ref(false)
const errorMessage = ref('')
const taskResult = ref(null)
//
let pollingTimer = null
//
const formData = reactive({
mode: 'EnvelopeOnly',
backupOriginal: true
})
//
const goBack = () => {
emit('page-change', 'analysis-tools')
}
//
const resetTask = () => {
taskStatus.value = 'idle'
currentTaskId.value = null
taskResult.value = null
errorMessage.value = ''
}
//
const stopPolling = () => {
if (pollingTimer) {
clearInterval(pollingTimer)
pollingTimer = null
}
}
//
const startTask = async () => {
isStarting.value = true
errorMessage.value = ''
try {
const res = await revitApi.executeShellOptimization(formData.mode, formData.backupOriginal)
if (res.success && res.data && res.data.code === 202) {
currentTaskId.value = res.data.data.taskId
taskStatus.value = 'Pending'
//
startPolling()
ElNotification({
title: '任务已创建',
message: '轻量化任务已提交,后台开始处理。',
type: 'success'
})
} else {
taskStatus.value = 'failed'
errorMessage.value = res.error || '创建任务失败,返回数据异常。'
}
} catch (err) {
taskStatus.value = 'failed'
errorMessage.value = err.message || '网络请求错误'
} finally {
isStarting.value = false
}
}
//
const startPolling = () => {
stopPolling() //
pollingTimer = setInterval(async () => {
if (!currentTaskId.value) return
try {
const res = await revitApi.getTaskStatus(currentTaskId.value)
if (res.success && res.data && res.data.data) {
const data = res.data.data
taskStatus.value = data.status
if (data.status === 'Completed') {
stopPolling()
taskResult.value = data.result
ElNotification({
title: '处理完成',
message: '模型轻量化处理已成功执行完成!',
type: 'success'
})
} else if (data.status === 'Failed') {
stopPolling()
errorMessage.value = data.errorMessage || '处理过程中出现未知错误'
ElNotification({
title: '处理失败',
message: errorMessage.value,
type: 'error'
})
} else if (data.status === 'Cancelled') {
stopPolling()
}
}
} catch (err) {
console.error('Task status polling failed:', err)
//
stopPolling()
taskStatus.value = 'failed'
errorMessage.value = '轮询状态失败: ' + err.message
}
}, 2000) // 2
}
//
const cancelTask = async () => {
if (!currentTaskId.value) return
try {
const res = await revitApi.cancelTask(currentTaskId.value)
if (res.success) {
taskStatus.value = 'Cancelled'
stopPolling()
ElNotification({
title: '已取消',
message: '任务已被成功取消。',
type: 'info'
})
}
} catch (err) {
ElNotification({
title: '取消失败',
message: err.message,
type: 'warning'
})
}
}
//
onUnmounted(() => {
stopPolling()
})
</script>
<style scoped>
.revit-envelope-optimization {
background: linear-gradient(135deg, var(--color-bg-secondary) 0%, var(--color-bg-primary) 100%);
border: 1px solid var(--color-border-primary);
border-radius: 16px;
padding: 24px;
backdrop-filter: blur(20px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
animation: slideInUp 0.4s ease-out;
color: var(--color-text-primary);
min-height: 100%;
}
.header-actions {
margin-bottom: 20px;
}
.icon-btn {
background: transparent;
border: none;
color: var(--color-primary);
font-size: 1.1em;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s;
}
.icon-btn:hover {
transform: translateX(-4px);
color: #5580ff;
}
.optimization-header {
text-align: center;
margin-bottom: 32px;
}
.optimization-header h2 {
color: var(--color-primary);
font-size: 1.8em;
font-weight: 600;
margin: 0 0 12px 0;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.optimization-header .subtitle {
color: var(--color-text-secondary);
font-size: 1em;
max-width: 600px;
margin: 0 auto;
line-height: 1.6;
}
/* 设定区 */
.settings-panel {
background: var(--color-bg-card);
border-radius: 12px;
padding: 32px;
max-width: 800px;
margin: 0 auto;
border: 1px solid var(--color-border-primary);
}
.setting-group {
margin-bottom: 32px;
}
.setting-group > label {
display: block;
font-size: 1.1em;
font-weight: 600;
margin-bottom: 16px;
color: var(--color-text-primary);
}
.mode-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
}
.mode-card {
display: block;
position: relative;
border: 2px solid var(--color-border-primary);
border-radius: 12px;
padding: 16px;
cursor: pointer;
transition: all 0.3s;
background: var(--color-bg-secondary);
}
.mode-card:hover {
border-color: var(--color-primary);
transform: translateY(-2px);
}
.mode-card.active {
border-color: var(--color-primary);
background: linear-gradient(135deg, rgba(58, 114, 255, 0.1) 0%, rgba(58, 114, 255, 0.05) 100%);
box-shadow: 0 4px 15px rgba(58, 114, 255, 0.2);
}
.mode-card input[type="radio"] {
display: none;
}
.mode-content h5 {
margin: 0 0 8px 0;
font-size: 1.1em;
color: var(--color-text-primary);
}
.mode-content p {
margin: 0;
font-size: 0.85em;
color: var(--color-text-secondary);
}
.toggle-group {
background: var(--color-bg-secondary);
padding: 16px;
border-radius: 12px;
}
.toggle-control {
display: flex;
justify-content: space-between;
align-items: center;
}
.action-bar {
text-align: center;
margin-top: 32px;
}
.primary-btn {
background: linear-gradient(135deg, #3a72ff 0%, #2f5ce6 100%);
color: white;
border: none;
border-radius: 8px;
padding: 14px 40px;
font-size: 1.1em;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 15px rgba(58, 114, 255, 0.4);
transition: all 0.3s;
display: flex;
align-items: center;
gap: 10px;
margin: 0 auto;
}
.primary-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(58, 114, 255, 0.6);
}
.primary-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.secondary-btn {
background: var(--color-bg-secondary);
color: var(--color-text-primary);
border: 1px solid var(--color-border-primary);
border-radius: 8px;
padding: 14px 40px;
font-size: 1.1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 10px;
}
.secondary-btn:hover {
background: var(--color-white-rgb-1);
}
.error-message {
margin-top: 20px;
padding: 12px;
background: rgba(255, 73, 73, 0.1);
color: #ff4949;
border-left: 4px solid #ff4949;
border-radius: 4px;
}
/* 执行进度区 */
.execution-panel {
text-align: center;
padding: 60px 20px;
}
.execution-panel h3 {
color: var(--color-primary);
font-size: 1.6em;
margin-bottom: 16px;
}
.status-desc {
color: var(--color-text-secondary);
font-size: 1.1em;
margin-bottom: 40px;
}
.progress-container {
max-width: 500px;
margin: 0 auto 40px auto;
}
.progress-bar {
height: 8px;
background: var(--color-border-primary);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3a72ff, #00d2ff);
width: 50%;
border-radius: 4px;
}
.progress-fill.active {
animation: progressIndeterminate 2s infinite ease-in-out;
transform-origin: 0% 50%;
}
@keyframes progressIndeterminate {
0% { transform: translateX(-100%) scaleX(0.2); }
50% { transform: translateX(0) scaleX(1); }
100% { transform: translateX(200%) scaleX(0.2); }
}
.cancel-btn {
background: transparent;
color: #ff4949;
border: 1px solid #ff4949;
border-radius: 6px;
padding: 10px 24px;
font-size: 1em;
cursor: pointer;
transition: all 0.2s;
}
.cancel-btn:hover {
background: rgba(255, 73, 73, 0.1);
}
/* 结果区 */
.result-panel {
max-width: 900px;
margin: 0 auto;
}
.success-header {
text-align: center;
margin-bottom: 40px;
}
.success-icon {
font-size: 64px;
color: #13ce66;
margin-bottom: 16px;
text-shadow: 0 4px 15px rgba(19, 206, 102, 0.3);
}
.success-header h3 {
font-size: 1.8em;
color: var(--color-text-primary);
margin: 0;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 24px;
margin-bottom: 32px;
}
.metric-card {
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
border-radius: 12px;
padding: 24px;
display: flex;
align-items: center;
gap: 20px;
}
.metric-icon {
font-size: 32px;
color: var(--color-primary);
width: 60px;
height: 60px;
background: rgba(58, 114, 255, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.metric-info {
flex: 1;
}
.metric-value {
font-size: 1.8em;
font-weight: 700;
color: var(--color-text-primary);
}
.metric-value.highlight {
color: #13ce66;
}
.metric-label {
color: var(--color-text-secondary);
font-size: 0.9em;
margin-top: 4px;
}
.details-box {
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
border-radius: 12px;
padding: 24px;
margin-bottom: 40px;
}
.details-box h4 {
margin: 0 0 20px 0;
color: var(--color-text-primary);
font-size: 1.2em;
}
.size-comparison {
display: flex;
align-items: center;
justify-content: space-around;
background: var(--color-bg-secondary);
padding: 24px;
border-radius: 8px;
margin-bottom: 20px;
}
.size-item {
text-align: center;
}
.size-item span {
display: block;
color: var(--color-text-secondary);
margin-bottom: 8px;
}
.size-item strong {
font-size: 1.6em;
color: var(--color-text-primary);
}
.size-item.optimized strong {
color: #13ce66;
}
.size-arrow i {
font-size: 24px;
color: var(--color-border-primary);
}
.backup-info {
color: var(--color-text-secondary);
font-size: 0.95em;
display: flex;
align-items: center;
gap: 8px;
}
.backup-info span {
color: var(--color-primary);
word-break: break-all;
}
.result-actions {
display: flex;
justify-content: center;
gap: 20px;
}
</style>

View File

@ -111,7 +111,9 @@ const CAD_SOFTWARE_DEFINITIONS = {
connect: '/api/health',
overview: '/api/overview',
shellAnalysis: '/api/shell/analyze',
exportIfc: '/api/export/ifc'
exportIfc: '/api/export/ifc',
shellExecute: '/api/shell/execute',
taskStatus: '/api/task'
}
},
PDMS: {

View File

@ -12,12 +12,14 @@ export const PAGE_TYPES = {
GEOMETRY_COMPLEXITY_RESULT: 'geometry-complexity-result',
GEOMETRY_OPTIMIZATION_PARAMS: 'geometry-optimization-params',
GEOMETRY_OPTIMIZATION_RESULT: 'geometry-optimization-result',
FILE_MANAGEMENT: 'file-management'
FILE_MANAGEMENT: 'file-management',
REVIT_ENVELOPE_OPTIMIZATION: 'revit-envelope-optimization'
}
// 需要CAD连接的页面
export const CAD_REQUIRED_PAGES = [
PAGE_TYPES.MODEL_VIEWER,
PAGE_TYPES.ANALYSIS_TOOLS,
PAGE_TYPES.EXPORT_TOOLS
PAGE_TYPES.EXPORT_TOOLS,
PAGE_TYPES.REVIT_ENVELOPE_OPTIMIZATION
]

View File

@ -78,6 +78,58 @@ class RevitApiService {
}
})
}
/**
* 发起删除任务(保留轮廓)
* @param {string} mode - Conservative | Standard | Aggressive | EnvelopeOnly
* @param {boolean} backupOriginal - 是否备份原文件true/false
* @returns {Promise<{success: boolean, data?: any, error?: string}>}
*/
async executeShellOptimization(mode = 'EnvelopeOnly', backupOriginal = true) {
const url = buildApiUrl(this.softwareName, 'shellExecute')
return await apiClient.post(url, {
mode,
backupOriginal
}, {
operationContext: {
software: 'Revit',
operation: '模型轻量化处理'
}
})
}
/**
* 轮询任务状态
* @param {string} taskId - 任务ID
* @returns {Promise<{success: boolean, data?: any, error?: string}>}
*/
async getTaskStatus(taskId) {
const baseUrl = buildApiUrl(this.softwareName, 'taskStatus')
const url = `${baseUrl}/${taskId}`
return await apiClient.get(url, {
operationContext: {
software: 'Revit',
operation: '获取当前任务状态',
silent: true
}
})
}
/**
* 取消任务
* @param {string} taskId - 任务ID
* @returns {Promise<{success: boolean, data?: any, error?: string}>}
*/
async cancelTask(taskId) {
const baseUrl = buildApiUrl(this.softwareName, 'taskStatus')
const url = `${baseUrl}/${taskId}`
return await apiClient.delete(url, null, {
operationContext: {
software: 'Revit',
operation: '取消轻量化任务'
}
})
}
}
// 导出单例实例

View File

@ -65,6 +65,7 @@ import HierarchyDeletionParamsPage from '@/components/pages/HierarchyDeletionPar
import GeometryComplexityResult from '@/components/pages/GeometryComplexityResult.vue'
import GeometryOptimizationParams from '@/components/pages/GeometryOptimizationParams.vue'
import GeometryOptimizationResult from '@/components/pages/GeometryOptimizationResult.vue'
import RevitEnvelopeOptimization from '@/components/pages/RevitEnvelopeOptimization.vue'
import FileManagementPage from '@/components/pages/FileManagementPage.vue'
import InfoManagementPanel from '@/components/layout/InfoManagementPanel.vue'
import { PAGE_TYPES } from '@/config/pages'
@ -110,7 +111,8 @@ const pageComponentMap = {
[PAGE_TYPES.GEOMETRY_COMPLEXITY_RESULT]: GeometryComplexityResult,
[PAGE_TYPES.GEOMETRY_OPTIMIZATION_PARAMS]: GeometryOptimizationParams,
[PAGE_TYPES.GEOMETRY_OPTIMIZATION_RESULT]: GeometryOptimizationResult,
[PAGE_TYPES.FILE_MANAGEMENT]: FileManagementPage
[PAGE_TYPES.FILE_MANAGEMENT]: FileManagementPage,
[PAGE_TYPES.REVIT_ENVELOPE_OPTIMIZATION]: RevitEnvelopeOptimization
}
// computed