feat: introduce UniversalConverter组件,支持STP转GLTF和IFC转STP文件转换功能。

This commit is contained in:
sladro 2026-02-28 19:51:24 +08:00
parent 3cdb7298e6
commit ce85d70758
2 changed files with 160 additions and 19 deletions

View File

@ -69,14 +69,12 @@
</div>
<div class="converter-panel">
<div class="upload-section">
<div class="upload-section" v-if="currentMode === 'stp-to-gltf'">
<div class="upload-area" id="file-upload-area">
<i class="fas fa-cloud-upload-alt"></i>
<h4 v-if="currentMode === 'stp-to-gltf'">拖拽STP文件到此处</h4>
<h4 v-else>拖拽IFC文件到此处</h4>
<h4>拖拽STP文件到此处</h4>
<p>或点击选择文件</p>
<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">
<input type="file" id="stp-file-input" accept=".stp,.step" class="hidden-input">
<button class="upload-btn" @click="selectFile">
<i class="fas fa-folder-open"></i>
选择文件
@ -84,6 +82,17 @@
</div>
</div>
<div class="path-input-section" v-else>
<div class="input-group">
<label><i class="fas fa-file-import"></i> 输入IFC绝对路径</label>
<input type="text" v-model="ifcPath" placeholder="例如D:/CAD/input.ifc" class="path-input">
</div>
<div class="input-group">
<label><i class="fas fa-file-export"></i> 输出STP绝对路径</label>
<input type="text" v-model="stpPath" placeholder="例如D:/CAD/output.stp" class="path-input">
</div>
</div>
<div class="conversion-options">
<h4>转换选项</h4>
<!-- STP to GLTF Options -->
@ -148,27 +157,29 @@
</div>
<div class="conversion-actions">
<button class="action-btn primary" id="start-conversion" disabled>
<i class="fas fa-play"></i>
开始转换
<button class="action-btn primary" :disabled="!canConvert || isConverting" @click="startConversion">
<i :class="isConverting ? 'fas fa-spinner fa-spin' : 'fas fa-play'"></i>
{{ isConverting ? '转换中...' : '开始转换' }}
</button>
<button class="action-btn secondary">
<button class="action-btn secondary" @click="resetForm" :disabled="isConverting">
<i class="fas fa-undo"></i>
重置
</button>
</div>
</div>
<div class="conversion-progress" id="conversion-progress" v-show="false">
<div class="conversion-progress" v-show="isConverting || convertResult">
<div class="progress-content">
<div class="progress-icon">
<i class="fas fa-spinner fa-spin"></i>
<div class="progress-icon" :class="{ 'success-icon': convertResult === 'success', 'error-icon': convertResult === 'error' }">
<i v-if="isConverting" class="fas fa-spinner fa-spin"></i>
<i v-else-if="convertResult === 'success'" class="fas fa-check-circle"></i>
<i v-else-if="convertResult === 'error'" class="fas fa-times-circle"></i>
</div>
<h4>正在转换...</h4>
<div class="progress-bar">
<div class="progress-fill" id="conversion-progress-fill"></div>
<h4>{{ progressTitle }}</h4>
<div class="progress-bar" v-if="isConverting">
<div class="progress-fill" style="width: 100%; animation: pulse 1.5s infinite;"></div>
</div>
<p id="conversion-status">准备转换...</p>
<p>{{ progressStatus }}</p>
</div>
</div>
</div>
@ -176,14 +187,80 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import websocketService from '@/services/websocketService'
// - UI
const currentMode = ref('stp-to-gltf')
// IFC to STP
const ifcPath = ref('')
const stpPath = ref('')
const isConverting = ref(false)
const convertResult = ref('') // 'success', 'error', ''
const progressTitle = ref('准备转换...')
const progressStatus = ref('')
const canConvert = computed(() => {
if (currentMode.value === 'ifc-to-stp') {
return ifcPath.value.trim() !== '' && stpPath.value.trim() !== ''
}
return false // STP to GLTF
})
const selectFile = () => {
// Vue
// UI
}
const resetForm = () => {
ifcPath.value = ''
stpPath.value = ''
convertResult.value = ''
progressTitle.value = '准备转换...'
progressStatus.value = ''
}
const startConversion = () => {
if (currentMode.value === 'ifc-to-stp') {
isConverting.value = true
convertResult.value = ''
progressTitle.value = '正在转换...'
progressStatus.value = '已发送转换请求,后台处理中...'
websocketService.send({
type: "convert_ifc_to_stp",
ifc_path: ifcPath.value.trim(),
stp_path: stpPath.value.trim()
})
}
}
const handleWsMessage = (message) => {
if (!isConverting.value) return
// msg.type === "info" msg.data.stp_path
if (message.type === "info" && message.data?.stp_path) {
isConverting.value = false
convertResult.value = 'success'
progressTitle.value = '转换成功!'
progressStatus.value = `文件已保存至: ${message.data.stp_path} (耗时: ${message.data.duration_ms}ms)`
}
// msg.type === "error"
else if (message.type === "error") {
// error
isConverting.value = false
convertResult.value = 'error'
progressTitle.value = '转换失败'
progressStatus.value = message.message || '未知错误'
}
}
onMounted(() => {
websocketService.on('onMessage', handleWsMessage)
})
onUnmounted(() => {
websocketService.off('onMessage', handleWsMessage)
})
</script>
<style scoped>
@ -556,5 +633,66 @@ const selectFile = () => {
display: none;
}
/* 路径输入样式 */
.path-input-section {
display: flex;
flex-direction: column;
gap: 20px;
background: var(--color-bg-tertiary);
padding: 24px;
border-radius: 12px;
border: 1px solid var(--color-border-primary);
margin-bottom: 24px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.input-group label {
color: var(--color-text-primary);
font-size: 15px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.input-group label i {
color: var(--color-primary);
}
.path-input {
padding: 12px 16px;
border: 1px solid var(--color-border-primary);
border-radius: 8px;
background: var(--color-bg-input);
color: var(--color-text-primary);
font-size: 14px;
transition: all 0.3s ease;
}
.path-input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px var(--color-primary-rgb-2);
}
.progress-icon.success-icon i {
color: var(--color-success);
}
.progress-icon.error-icon i {
color: var(--color-danger);
}
@keyframes pulse {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}
/* 使用全局CSS变量 - 已在theme.css中统一定义 */
</style>

3
命令.md Normal file
View File

@ -0,0 +1,3 @@
### 打包
$env:Path += ";$env:USERPROFILE\.cargo\bin"
npm run tauri:build