feat: 实现Revit IFC导出功能并修复导出页面路由逻辑

- 新增RevitExportTools组件,支持完整IFC导出参数配置
- 在revitApi.js中添加exportIFC方法,调用/api/export/ifc端点
- 修复DashboardView导出页面路由,根据连接软件显示对应导出工具
- 修复组件样式问题,正确显示Revit绿色主题而非黑白灰
- 添加未连接CAD时的友好提示信息

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sladro 2025-09-18 18:42:38 +08:00
parent 5389e37ffe
commit 7785b1f1e4
3 changed files with 521 additions and 1 deletions

View File

@ -0,0 +1,492 @@
<template>
<div class="export-tools-container revit-theme">
<!-- 导出工具头部 -->
<div class="export-header">
<div class="header-content">
<div class="header-icon">
<i class="fas fa-download"></i>
</div>
<div class="header-text">
<h2>Revit IFC导出工具</h2>
<p>将当前Revit模型导出为IFC格式文件</p>
</div>
</div>
</div>
<!-- 当前软件信息 -->
<div class="export-card">
<div class="card-header">
<i class="fas fa-info-circle"></i>
<h3>当前连接信息</h3>
</div>
<div class="card-content">
<div class="connected-software-info">
<div class="software-display">
<div class="software-icon-large">
<i class="fas fa-building"></i>
</div>
<div class="software-details">
<h4>Revit</h4>
<p>连接状态: <span class="status-connected">已连接</span></p>
<p class="no-project">准备导出</p>
</div>
</div>
</div>
</div>
</div>
<!-- IFC导出设置 -->
<div class="export-card">
<div class="card-header">
<i class="fas fa-cog"></i>
<h3>IFC导出设置</h3>
</div>
<div class="card-content">
<p>配置IFC导出的参数和选项</p>
<div class="export-options">
<div class="option-group">
<label for="output-path">输出路径</label>
<input
type="text"
id="output-path"
v-model="exportForm.outputPath"
placeholder="D:\export.ifc"
class="form-input"
>
<small class="form-hint">请输入完整的文件路径包含文件名和.ifc扩展名</small>
</div>
<div class="option-group">
<label for="ifc-version">IFC版本</label>
<select id="ifc-version" v-model="exportForm.version">
<option value="0">IFC2x3</option>
<option value="1">IFC4</option>
</select>
<small class="form-hint">选择要导出的IFC格式版本</small>
</div>
<div class="option-group">
<label for="export-range">导出范围</label>
<select id="export-range" v-model="exportForm.range">
<option value="0">当前视图</option>
<option value="1">整个项目</option>
</select>
<small class="form-hint">选择要导出的模型范围</small>
</div>
<div class="option-group">
<label for="file-prefix">文件名前缀</label>
<input
type="text"
id="file-prefix"
v-model="exportForm.fileNamePrefix"
placeholder="MyExport"
class="form-input"
>
<small class="form-hint">可选的文件名前缀</small>
</div>
</div>
<div class="card-actions">
<button class="btn btn-export btn-lg btn-full" :disabled="!canExport" @click="startExport">
<i class="fas fa-download"></i>
开始导出IFC
</button>
</div>
</div>
</div>
<!-- Revit高级选项 -->
<div class="export-card revit-advanced-options">
<div class="card-header">
<i class="fas fa-building"></i>
<h3>Revit高级导出选项</h3>
</div>
<div class="card-content">
<p>配置Revit专属的IFC导出高级参数</p>
<div class="export-options">
<div class="option-group">
<label>
<input type="checkbox" v-model="exportForm.includeSpaceBoundaries">
包含空间边界
</label>
<small class="form-hint">在IFC文件中包含空间边界信息</small>
</div>
<div class="option-group">
<label>
<input type="checkbox" v-model="exportForm.splitWallsAndColumns">
分割墙和柱
</label>
<small class="form-hint">将墙和柱按层分割为独立元素</small>
</div>
<div class="option-group">
<label>
<input type="checkbox" v-model="exportForm.include2DElements">
包含2D元素
</label>
<small class="form-hint">导出2D图形元素和注释</small>
</div>
<div class="option-group">
<label for="level-names">指定层级</label>
<input
type="text"
id="level-names"
v-model="levelNamesText"
@input="updateLevelNames"
placeholder="Level 1, Level 2"
class="form-input"
>
<small class="form-hint">用逗号分隔的层级名称留空则导出所有层级</small>
</div>
<div class="option-group">
<label for="categories">指定类别</label>
<input
type="text"
id="categories"
v-model="categoriesText"
@input="updateCategories"
placeholder="Walls, Doors, Windows"
class="form-input"
>
<small class="form-hint">用逗号分隔的类别名称留空则导出所有类别</small>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, computed, ref } from 'vue'
import revitApi from '@/services/revitApi'
//
const exportForm = reactive({
outputPath: 'D:\\export.ifc',
version: 0,
range: 0,
includeSpaceBoundaries: false,
splitWallsAndColumns: false,
levelNames: [],
categories: [],
include2DElements: false,
fileNamePrefix: 'MyExport'
})
//
const levelNamesText = ref('')
const categoriesText = ref('')
//
const canExport = computed(() => {
return exportForm.outputPath && exportForm.outputPath.trim()
})
//
const updateLevelNames = () => {
exportForm.levelNames = levelNamesText.value
.split(',')
.map(name => name.trim())
.filter(name => name)
}
//
const updateCategories = () => {
exportForm.categories = categoriesText.value
.split(',')
.map(cat => cat.trim())
.filter(cat => cat)
}
//
const startExport = async () => {
if (!canExport.value) return
await revitApi.exportIFC({
outputPath: exportForm.outputPath,
version: exportForm.version,
range: exportForm.range,
includeSpaceBoundaries: exportForm.includeSpaceBoundaries,
splitWallsAndColumns: exportForm.splitWallsAndColumns,
levelNames: exportForm.levelNames,
categories: exportForm.categories,
include2DElements: exportForm.include2DElements,
fileNamePrefix: exportForm.fileNamePrefix
})
}
</script>
<style scoped>
/* Revit导出工具样式 - 复用主题变量使用Revit绿色主题 */
.export-tools-container {
padding: var(--model-viewer-padding);
max-width: 800px;
margin: 0 auto;
}
.export-header {
margin-bottom: var(--spacing-3xl);
}
.export-header .header-content {
display: flex;
align-items: center;
gap: var(--model-viewer-header-gap);
}
.export-header .header-icon {
width: 60px;
height: 60px;
background: linear-gradient(135deg, #2E8B57 0%, #20B2AA 100%);
border-radius: var(--size-border-radius-card);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-icon);
color: var(--color-text-primary);
box-shadow: 0 4px 12px rgba(46, 139, 87, 0.3);
}
.export-header .header-text h2 {
color: var(--color-text-primary);
margin: 0 0 var(--spacing-sm) 0;
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
}
.export-header .header-text p {
color: var(--color-text-secondary);
margin: 0;
font-size: var(--font-size-lg);
}
/* 连接软件信息显示 */
.connected-software-info {
margin-bottom: var(--spacing-lg);
}
.software-display {
display: flex;
align-items: center;
gap: var(--spacing-xl);
padding: var(--spacing-lg);
background: rgba(46, 139, 87, 0.1);
border: 1px solid rgba(46, 139, 87, 0.3);
border-radius: var(--size-border-radius);
}
.software-icon-large {
width: 60px;
height: 60px;
background: linear-gradient(135deg, #2E8B57 0%, #20B2AA 100%);
border-radius: var(--size-border-radius-card);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-3xl);
color: var(--color-text-primary);
flex-shrink: 0;
}
.software-details {
flex: 1;
}
.software-details h4 {
color: var(--color-text-primary);
margin: 0 0 var(--spacing-sm) 0;
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-semibold);
}
.software-details p {
color: var(--color-text-secondary);
margin: 0 0 var(--spacing-xs) 0;
font-size: var(--font-size-base);
}
.status-connected {
color: var(--color-success);
font-weight: var(--font-weight-semibold);
}
.no-project {
color: #2E8B57;
font-style: italic;
}
.export-card {
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
border-radius: var(--size-border-radius-card);
overflow: hidden;
margin-bottom: var(--spacing-xl);
}
.export-card .card-header {
background: var(--color-bg-tertiary);
padding: var(--spacing-xl);
display: flex;
align-items: center;
gap: var(--spacing-lg);
border-bottom: 1px solid var(--color-border-primary);
}
.export-card .card-header i {
font-size: var(--font-size-icon);
color: #2E8B57;
}
.export-card .card-header h3 {
color: var(--color-text-primary);
margin: 0;
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-semibold);
}
.export-card .card-content {
padding: var(--spacing-2xl);
}
.export-card .card-content p {
color: var(--color-text-secondary);
margin-bottom: var(--spacing-xl);
line-height: 1.6;
}
.export-options {
margin-bottom: var(--spacing-2xl);
}
.option-group {
margin-bottom: var(--spacing-lg);
}
.option-group label {
color: var(--color-text-primary);
font-weight: var(--font-weight-medium);
margin-bottom: var(--spacing-sm);
display: block;
}
.form-input {
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--color-bg-input);
border: 1px solid var(--color-border-primary);
border-radius: var(--size-border-radius);
color: var(--color-text-primary);
font-size: var(--font-size-base);
box-sizing: border-box;
transition: all var(--transition-duration) var(--transition-timing);
}
.form-input:focus {
outline: none;
border-color: #2E8B57;
box-shadow: 0 0 0 2px rgba(46, 139, 87, 0.1);
background: var(--color-white-rgb-05);
}
.form-hint {
color: var(--color-text-tertiary);
margin-top: var(--spacing-xs);
display: block;
font-size: var(--font-size-xs);
}
.option-group select {
width: 200px;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--color-bg-input);
border: 1px solid var(--color-border-primary);
border-radius: var(--size-border-radius);
color: var(--color-text-primary);
font-size: var(--font-size-base);
transition: all var(--transition-duration) var(--transition-timing);
}
.option-group select:focus {
outline: none;
border-color: #2E8B57;
box-shadow: 0 0 0 2px rgba(46, 139, 87, 0.1);
}
.option-group input[type="checkbox"] {
margin-right: var(--spacing-sm);
accent-color: #2E8B57;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-xl);
border: none;
border-radius: var(--size-border-radius-button);
font-size: var(--font-size-base);
font-weight: var(--font-weight-medium);
text-decoration: none;
cursor: pointer;
transition: all var(--transition-duration) var(--transition-timing);
user-select: none;
min-height: 40px;
white-space: nowrap;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.btn-export {
position: relative;
overflow: hidden;
background: linear-gradient(135deg, #2E8B57 0%, #20B2AA 100%);
border: none;
box-shadow: 0 4px 15px rgba(46, 139, 87, 0.3);
transition: all var(--transition-duration) cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
min-width: 150px;
color: var(--color-text-primary);
}
.btn-export:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(46, 139, 87, 0.4);
}
.btn-export:disabled {
background: var(--color-bg-quaternary);
box-shadow: none;
opacity: 0.7;
}
.btn-lg {
padding: var(--spacing-md) var(--spacing-2xl);
min-height: 48px;
font-size: var(--font-size-lg);
}
.btn-full {
width: 100%;
}
.card-actions {
display: flex;
gap: var(--spacing-lg);
}
.revit-advanced-options .card-header i {
color: #2E8B57;
}
</style>

View File

@ -62,6 +62,22 @@ class RevitApiService {
}
})
}
/**
* 导出IFC文件
* @param {Object} exportOptions - 导出选项
* @returns {Promise<{success: boolean, data?: any, error?: string}>}
*/
async exportIFC(exportOptions) {
const url = buildApiUrl(this.softwareName, 'exportIfc')
return await apiClient.post(url, exportOptions, {
operationContext: {
software: 'Revit',
operation: 'IFC导出'
}
})
}
}
// 导出单例实例

View File

@ -125,7 +125,8 @@
<!-- 导出工具页面 -->
<div v-else-if="currentPage === PAGE_TYPES.EXPORT_TOOLS" class="page-content">
<CreoExportTools v-if="currentConnectedSoftware === 'creo'" />
<CreoExportTools v-else />
<RevitExportTools v-else-if="currentConnectedSoftware === 'revit'" />
<div v-else class="no-connection">请先连接CAD软件</div>
</div>
<!-- 格式转换页面 -->
@ -273,6 +274,7 @@ import RevitModelViewer from '@/components/model/RevitModelViewer.vue'
import RevitAnalysisDashboard from '@/components/pages/RevitAnalysisDashboard.vue'
import CreoModelAnalysis from '@/components/pages/CreoModelAnalysis.vue'
import CreoExportTools from '@/components/pages/CreoExportTools.vue'
import RevitExportTools from '@/components/pages/RevitExportTools.vue'
import UniversalConverter from '@/components/pages/UniversalConverter.vue'
import ShellAnalysisResult from '@/components/pages/ShellAnalysisResult.vue'
import HierarchyAnalysisResult from '@/components/pages/HierarchyAnalysisResult.vue'
@ -401,6 +403,16 @@ const closeInfoPanel = () => {
width: 100%;
}
.no-connection {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--color-text-secondary);
font-size: 16px;
text-align: center;
}
/* 连接页面样式 */
.connection-page {
display: flex;