feat: 实现批量任务管理页面,包含任务创建向导、任务列表、状态展示、报告日志查看器及Revit模型查看器,并新增CAD数据存储。

This commit is contained in:
sladro 2026-03-02 16:34:37 +08:00
parent a837baef95
commit b53f9d37e5
5 changed files with 465 additions and 8 deletions

View File

@ -10,6 +10,24 @@
- **Base URL**: `http://localhost:12345`
- **端口**: `12345`
### 1.0. 插件可用性探活 API (Health Check)
- **接口路径**: `/api/health` (或 `/api/status`)
- **请求方式**: `GET`
- **基础描述**: 用于队列执行前快速判活,检测 Creo 插件是否正常启动并可以接收任务,减少无效重试。
- **接入说明**:
- **请求参数**: 无
- **响应格式**:
```json
{
"status": "success",
"message": "Creo 插件在线就绪",
"data": {
"isBusy": false // 是否正忙于处理其他任务
}
}
```
- **异常响应**: 如插件未启动、断联或崩溃,通常表现为连接超时或 HTTP 50x 错误。
### 1.1. 打开模型 API
- **接口路径**: `/api/model/open`
- **请求方式**: `POST`
@ -81,6 +99,24 @@
- **Base URL**: `http://localhost:9000`
- **端口**: `9000`
### 2.0. 插件可用性探活 API (Health Check)
- **接口路径**: `/api/health` (或 `/api/status`)
- **请求方式**: `GET`
- **基础描述**: 用于队列执行前快速判活,检测 Revit 插件是否正常启动并可以接收任务,减少无效重试。
- **接入说明**:
- **请求参数**: 无
- **响应格式**:
```json
{
"status": "success",
"message": "Revit 插件在线就绪",
"data": {
"isBusy": false
}
}
```
- **异常响应**: 如插件未启动、断联或崩溃,通常表现为连接超时或 HTTP 50x 错误。
### 2.1. 打开模型 API
- **接口路径**: `/api/open`
- **请求方式**: `POST`
@ -131,6 +167,24 @@
- **Base URL**: `http://localhost:9001`
- **端口**: `9001`
### 3.0. 插件可用性探活 API (Health Check)
- **接口路径**: `/api/health` (或 `/api/status`)
- **请求方式**: `GET`
- **基础描述**: 用于队列执行前快速判活,检测 PDMS 插件是否正常启动并可以接收任务,减少无效重试。
- **接入说明**:
- **请求参数**: 无
- **响应格式**:
```json
{
"status": "success",
"message": "PDMS 插件在线就绪",
"data": {
"isBusy": false
}
}
```
- **异常响应**: 如插件未启动、断联或崩溃,通常表现为连接超时或 HTTP 50x 错误。
### 3.1. 打开模型 API
- **接口路径**: `/api/project/open` (打开工程) / `/api/mdb/open` (打开 MDB 数据库)
- **请求方式**: `POST`

View File

@ -0,0 +1,45 @@
# CAD 插件可用性探活 API 接入文档
为了在队列执行前快速判断各 CADCreo、Revit、PDMS插件是否在线、能否接收任务从而减少无效的接口重试特提供以下统一的探活Health CheckAPI 说明。后端开发在接入调度队列前,可先通过本接口进行快速判活。
## 1. 统一接口规范
对所有 CAD 插件,推荐的探活接口标准如下,后端可统一做异常和状态处理:
- **典型接口路径**: `/api/health``/api/status`
- **请求方式**: `GET`
- **请求参数**: 无
- **请求头**: 无特殊要求
- **成功响应格式**:
```json
{
"status": "success",
"message": "[CAD软件名称] 插件在线就绪",
"data": {
"isBusy": false
}
}
```
- `data.isBusy`: (Boolean) `false` 表示空闲,可以接受新任务;`true` 表示当前插件正在忙于处理其他任务(例如正在解析一个非常大的模型)。队列调度时,可以结合此字段进行等待、延迟派发或抛出忙碌异常。
- **异常表现**:
- 如果插件崩溃、未启动或网络断联,该接口通常会直接抛出 **连接超时 (Timeout)****HTTP 502/503/500/拒绝连接** 错误。
- 队列调度进行预检查时,若捕获到任何网络层或 HTTP 非 2xx 异常,应即刻将当前节点判断为离线/不健康。
---
## 2. 各个 CAD 软件对接详情
各个 CAD 插件的基础地址和端口号不尽相同,后端组装对应 URL 发送请求测试连通性。
### 2.1. Creo Parametric
- **当前 Base URL**: `http://localhost:12345` _(根据实际部署调整)_
- **完整探活请求**: `GET http://localhost:12345/api/health`
### 2.2. Revit
- **当前 Base URL**: `http://localhost:9000` _(根据实际部署调整)_
- **完整探活请求**: `GET http://localhost:9000/api/health`
### 2.3. PDMS
- **当前 Base URL**: `http://localhost:9001` _(根据实际部署调整)_
- **完整探活请求**: `GET http://localhost:9001/api/health`

View File

@ -159,7 +159,7 @@
<div v-show="wizardStep === 1" class="step-panel">
<el-form :model="taskForm" label-width="120px">
<el-form-item label="执行策略序列">
<el-select v-model="taskForm.strategy" placeholder="请选择策略" style="width: 100%">
<el-select v-model="taskForm.strategy" placeholder="请选择策略" style="width: 100%" @change="onStrategyChange">
<el-option label="模型打开 -> Shrinkwrap外壳 -> 关闭模型 (Creo)" value="creo_shrinkwrap" />
<el-option label="模型打开 -> 建筑模型剥壳 -> 关闭模型 (Revit)" value="revit_shell" />
<el-option label="工程打开 -> 模型包裹化防泄密 -> 占位关闭 (PDMS)" value="pdms_shrinkwrap" />
@ -169,6 +169,51 @@
<el-form-item label="策略说明">
<span class="form-tip" style="margin-left:0;">提交时会将策略转化为相应的 task_type 序述串通过 WebSocket 下发</span>
</el-form-item>
<div class="strategy-params" v-if="taskForm.strategy">
<el-divider content-position="left"><i class="fas fa-sliders-h"></i> 策略级参数配置</el-divider>
<template v-if="taskForm.strategy === 'creo_shrinkwrap'">
<el-form-item label="轻量化质量">
<el-slider v-model="taskForm.params.quality" :min="1" :max="10" show-input style="max-width: 400px;"></el-slider>
</el-form-item>
<el-form-item label="导出格式">
<el-select v-model="taskForm.params.exportFormat" placeholder="输出格式" style="width: 200px">
<el-option label="默认 (.prt / .asm)" value="default" />
<el-option label="STEP (.stp)" value="step" />
<el-option label="IGES (.igs)" value="iges" />
<el-option label="OBJ (.obj)" value="obj" />
</el-select>
</el-form-item>
<el-form-item label="高级过滤">
<el-checkbox v-model="taskForm.params.ignoreSkeletons">忽略骨架</el-checkbox>
<el-checkbox v-model="taskForm.params.ignoreQuilts">忽略面组 (Quilts)</el-checkbox>
<el-checkbox v-model="taskForm.params.fillHoles">自动补孔</el-checkbox>
</el-form-item>
</template>
<template v-else-if="taskForm.strategy === 'revit_shell'">
<el-form-item label="细节级别 (LOD)">
<el-radio-group v-model="taskForm.params.lod">
<el-radio label="粗糙">粗糙 (Coarse)</el-radio>
<el-radio label="中等">中等 (Medium)</el-radio>
<el-radio label="精细">精细 (Fine)</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="移除内部构件">
<el-switch v-model="taskForm.params.removeInternal" active-text="" inactive-text="" />
</el-form-item>
</template>
<template v-else-if="taskForm.strategy.includes('pdms')">
<el-form-item label="层级保留">
<el-input v-model="taskForm.params.levelFilter" placeholder="例如: /SITE/ZONE" style="width: 250px" />
</el-form-item>
<el-form-item label="移除管道附件">
<el-switch v-model="taskForm.params.removeAttachments" />
</el-form-item>
</template>
</div>
</el-form>
</div>
@ -212,7 +257,8 @@ const wizardStep = ref(0)
const taskForm = ref({
strategy: 'creo_shrinkwrap',
priority: '普通',
modelPaths: []
modelPaths: [],
params: { quality: 5, exportFormat: 'step', ignoreSkeletons: true, ignoreQuilts: false, fillHoles: false }
})
const newModelPath = ref('')
@ -285,11 +331,24 @@ const openWizard = () => {
taskForm.value = {
strategy: 'creo_shrinkwrap',
priority: '普通',
modelPaths: []
modelPaths: [],
params: { quality: 5, exportFormat: 'step', ignoreSkeletons: true, ignoreQuilts: false, fillHoles: false }
}
wizardVisible.value = true
}
const onStrategyChange = (val) => {
if (val === 'creo_shrinkwrap') {
taskForm.value.params = { quality: 5, exportFormat: 'step', ignoreSkeletons: true, ignoreQuilts: false, fillHoles: false }
} else if (val === 'revit_shell') {
taskForm.value.params = { lod: '粗糙', removeInternal: true }
} else if (val.includes('pdms')) {
taskForm.value.params = { levelFilter: '', removeAttachments: true }
} else {
taskForm.value.params = {}
}
}
const submitTask = () => {
if (taskForm.value.modelPaths.length === 0) {
ElMessage.warning('请先添加至少一个模型路径')
@ -309,6 +368,7 @@ const submitTask = () => {
batchId: `B-${Date.now().toString().slice(-8)}`,
strategy: taskForm.value.strategy,
priority: taskForm.value.priority,
params: { ...taskForm.value.params },
progress: 0,
status: 'queued',
totalCount: taskForm.value.modelPaths.length,
@ -576,6 +636,19 @@ const viewDetails = (row) => {
color: var(--color-text-tertiary);
}
.strategy-params {
background: var(--color-bg-card-secondary, rgba(255,255,255,0.05));
border-radius: var(--size-border-radius);
padding: var(--spacing-md) var(--spacing-lg);
margin-top: var(--spacing-md);
border: 1px dashed var(--color-border-primary);
}
.strategy-params .el-divider {
margin-top: 0;
margin-bottom: var(--spacing-lg);
}
/* 覆盖 Element UI 为深色主题适配 */
:deep(.el-table) {
--el-table-bg-color: transparent;

View File

@ -118,13 +118,112 @@
<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-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-group>
</div>
<!-- 实时模式内容 -->
<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="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">
<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>
</ul>
</div>
</div>
</div>
<!-- 步骤 2: 执行轻量化 -->
<div class="flow-step" :class="{ 'step-active': liveStep === 2, 'step-done': liveStep > 2 }">
<div class="step-header">
<div class="step-num">2</div>
<h3>执行轻量化处理</h3>
</div>
<div class="step-content">
<div v-if="liveStep <= 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>
<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 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>
</ul>
</div>
<div class="vs-divider">VS</div>
<div class="comp-col target-col">
<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>
</ul>
</div>
</div>
<div class="mt-4 flex-center">
<el-button plain @click="resetLiveFlow">开启新一轮对比</el-button>
</div>
</div>
</transition>
</div>
</div>
</div>
</div>
<!-- 自动模式内容 -->
<div v-if="comparisonMode === 'auto'" class="auto-comparison">
<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>
@ -209,7 +308,58 @@ import { ElMessage } from 'element-plus'
const searchQuery = ref('')
const activeTab = ref('overview')
const comparisonMode = ref('auto')
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 = () => {
isCapturingBaseline.value = true
setTimeout(() => {
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 = () => {
isCapturingResult.value = true
setTimeout(() => {
isCapturingResult.value = false
resultData.value = {
mem: '490 MB',
poly: '682,100',
features: '342'
}
liveStep.value = 4
ElMessage.success('最新状态捕获及对比报告已生成')
}, 1000)
}
const resetLiveFlow = () => {
liveStep.value = 1
baselineData.value = null
resultData.value = null
}
//
const mockReports = [
@ -749,4 +899,134 @@ const exportSelectedReport = () => {
.manual-actions {
text-align: center;
}
/* 实时流程逻辑样式 */
.live-flow-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.flow-step {
background: var(--color-bg-primary);
border: 1px solid var(--color-border-primary);
border-radius: 12px;
padding: 20px;
opacity: 0.5;
transition: all 0.3s;
}
.flow-step.step-active {
opacity: 1;
border-color: var(--color-primary);
box-shadow: 0 0 10px rgba(var(--color-primary-rgb), 0.1);
}
.flow-step.step-done {
opacity: 0.8;
border-color: var(--color-success);
}
.step-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.step-num {
width: 32px;
height: 32px;
background: var(--color-white-rgb-05);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: var(--color-text-secondary);
}
.step-active .step-num {
background: var(--color-primary);
color: #fff;
}
.step-done .step-num {
background: var(--color-success);
color: #fff;
}
.step-header h3 {
margin: 0;
font-size: 16px;
color: var(--color-text-primary);
}
.step-content {
padding-left: 44px;
}
.live-btn {
border-radius: 8px;
min-width: 200px;
}
.wait-msg {
color: var(--color-text-secondary);
font-size: 14px;
margin-bottom: 12px;
}
.success-msg {
color: var(--color-text-primary);
font-size: 14px;
}
.success-msg i {
margin-right: 6px;
font-size: 16px;
}
.captured-data {
padding: 16px;
background: rgba(var(--color-primary-rgb), 0.05);
border: 1px dashed var(--color-primary);
max-width: 400px;
}
.captured-data h4 {
margin: 0 0 12px 0;
font-size: 15px;
}
.data-list {
list-style: none;
padding: 0;
margin: 0;
}
.data-list li {
display: flex;
margin-bottom: 8px;
font-size: 14px;
}
.data-list .lbl {
color: var(--color-text-secondary);
width: 80px;
}
.data-list .val {
color: var(--color-text-primary);
font-weight: 500;
}
.mt-4 {
margin-top: 16px;
}
.flex-center {
display: flex;
justify-content: center;
}
</style>

View File

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