feat: 新增通用转换器组件,支持STP转GLTF、IFC转STP及IFC批量转STP功能,并添加批量任务管理器和相关文档。
This commit is contained in:
parent
ce85d70758
commit
a837baef95
168
CAD_API_集成文档.md
Normal file
168
CAD_API_集成文档.md
Normal file
@ -0,0 +1,168 @@
|
||||
# CAD 软件模型处理 API 集成文档
|
||||
|
||||
本文档详细说明了不同 CAD 软件(Creo, Revit, PDMS)在本项目中的 API 接入规范,包括基础配置、核心模型操作以及类似 Shrink Wrap(几何优化轻量化)功能的接口文档。请按照以下说明进行前后端集成。
|
||||
|
||||
---
|
||||
|
||||
## 1. Creo Parametric
|
||||
|
||||
- **CAD 软件名称**: Creo Parametric (版本 9.0)
|
||||
- **Base URL**: `http://localhost:12345`
|
||||
- **端口**: `12345`
|
||||
|
||||
### 1.1. 打开模型 API
|
||||
- **接口路径**: `/api/model/open`
|
||||
- **请求方式**: `POST`
|
||||
- **基础描述**: 在 Creo 软件中加载指定的模型文件。
|
||||
- **接入说明**:
|
||||
- **请求头**: `Content-Type: application/json`
|
||||
- **请求体 (Body)**:
|
||||
```json
|
||||
{
|
||||
"filePath": "C:\\path\\to\\your\\model.prt" // 字符串,必须是绝对路径
|
||||
}
|
||||
```
|
||||
- **响应格式**:
|
||||
```json
|
||||
{
|
||||
"status": "success", // 或 "error"
|
||||
"message": "模型加载成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
- **常见错误**: 若路劲不存在或 Creo 实例未启动,status 会返回 error。
|
||||
|
||||
### 1.2. 几何优化分析 API (类似 Shrink Wrap 功能)
|
||||
- **接口路径**: `/api/creo/shrinkwrap/shell` (及 `/api/analysis/shell-analysis`)
|
||||
- **请求方式**: `POST`
|
||||
- **基础描述**: 对打开的模型执行几何外壳提取或包络处理,用于轻量化。
|
||||
- **接入说明**:
|
||||
- **请求头**: `Content-Type: application/json`
|
||||
- **请求体 (Body - 参数示例)**:
|
||||
```json
|
||||
{
|
||||
"method": "outer_shell", // 提取方法:外壳
|
||||
"quality": 5, // 提取质量 (1-10)
|
||||
"chord_height": 0.15, // 弦高公差
|
||||
"fill_holes": false, // 是否填充孔洞
|
||||
"ignore_small_surfaces": false, // 是否忽略小曲面
|
||||
"output_type": "solid_surface" // 输出类型
|
||||
}
|
||||
```
|
||||
- **响应格式**:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "几何优化完成",
|
||||
"data": {
|
||||
"output_file": "C:\\path\\to\\optimized_model.prt"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3. 关闭模型 API
|
||||
- **接口路径**: `[占位待实现] /api/model/close`
|
||||
- **请求方式**: `POST`
|
||||
- **基础描述**: 关闭当前在 Cero 中处于激活状态的模型,释放内存。
|
||||
- **接入说明**:
|
||||
- 当前后端尚未提供明确的单独关闭接口。
|
||||
- **未来预期请求体**:
|
||||
```json
|
||||
{
|
||||
"clearMemory": true // 是否连带清除缓存
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Revit
|
||||
|
||||
- **CAD 软件名称**: Revit (版本 2024)
|
||||
- **Base URL**: `http://localhost:9000`
|
||||
- **端口**: `9000`
|
||||
|
||||
### 2.1. 打开模型 API
|
||||
- **接口路径**: `/api/open`
|
||||
- **请求方式**: `POST`
|
||||
- **基础描述**: 打开指定的 Revit 项目文件 (.rvt)。
|
||||
- **接入说明**:
|
||||
- **请求格式**: `application/json`
|
||||
- **请求体 (Body)**:
|
||||
```json
|
||||
{
|
||||
"filePath": "C:\\path\\to\\project.rvt"
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2. 轻量化提取外壳 API (类似 Shrink Wrap 功能)
|
||||
- **接口路径**: `/api/shell/analyze` (提取分析) / `/api/shell/execute` (执行处理)
|
||||
- **请求方式**: `POST`
|
||||
- **基础描述**: 分析 Revit 建筑模型并剥离内部不需要的实体细节。
|
||||
- **接入说明**:
|
||||
- **请求体 (Body)**:
|
||||
```json
|
||||
{
|
||||
"detailLevel": "coarse", // 粗略级别
|
||||
"removeInternal": true // 是否移除内部构件
|
||||
}
|
||||
```
|
||||
- **响应格式**:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"taskId": "task-123456" // 大模型处理可能较慢,建议通过 /api/task 轮询进度
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3. 关闭模型 API
|
||||
- **接口路径**: `[占位待实现] /api/close`
|
||||
- **请求方式**: `POST`
|
||||
- **基础描述**: 关闭 Revit 当前工程并释放资源。
|
||||
- **接入说明**:
|
||||
- 当前未列出明确接口,前端可作为占位按钮。
|
||||
|
||||
---
|
||||
|
||||
## 3. PDMS
|
||||
|
||||
- **CAD 软件名称**: PDMS (版本 12.1)
|
||||
- **Base URL**: `http://localhost:9001`
|
||||
- **端口**: `9001`
|
||||
|
||||
### 3.1. 打开模型 API
|
||||
- **接口路径**: `/api/project/open` (打开工程) / `/api/mdb/open` (打开 MDB 数据库)
|
||||
- **请求方式**: `POST`
|
||||
- **基础描述**: 载入 PDMS 的工程或特定 MDB 库。
|
||||
- **接入说明**:
|
||||
- **请求体 (Body)**:
|
||||
```json
|
||||
{
|
||||
"projectName": "SAM",
|
||||
"username": "SYSTEM",
|
||||
"password": "XXXXXX",
|
||||
"systemName": "CATA"
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2. 模型简化与包裹 API (类似 Shrink Wrap 功能)
|
||||
- **接口路径**: `/api/model/shrinkwrap` (包裹) / `/api/model/simplify` (参数化简化)
|
||||
- **请求方式**: `POST`
|
||||
- **基础描述**: 将复杂的管道、设备利用包围盒或简化几何进行替代。
|
||||
- **接入说明**:
|
||||
- **请求体 (Body) - Simplify 示例**:
|
||||
```json
|
||||
{
|
||||
"targetElements": ["/EQUIP-1", "/PIPE-2"],
|
||||
"simplifyMethod": "bounding_box"
|
||||
}
|
||||
```
|
||||
- **响应**: 处理成功后通常会通过 WebSocket 推送更新或直接返回简化的模型状态数据。
|
||||
|
||||
### 3.3. 关闭模型 API
|
||||
- **接口路径**: `[占位待实现] /api/project/close`
|
||||
- **请求方式**: `POST`
|
||||
- **基础描述**: 退出工程并回收当前资源。
|
||||
- **接入说明**:
|
||||
- 占位状态,开发集成时需要确保该请求不会导致整个 PDMS 进程意外崩溃。
|
||||
155
docs/cad-batch-plan.md
Normal file
155
docs/cad-batch-plan.md
Normal file
@ -0,0 +1,155 @@
|
||||
# CAD 批处理串行执行框架(设计与开发进度)
|
||||
|
||||
## 1. 文档目的
|
||||
本文件用于同步两类信息:
|
||||
1. 总体批处理设计(长期有效)
|
||||
2. 当前代码开发进度(持续更新)
|
||||
|
||||
---
|
||||
|
||||
## 2. 总体设计(已定稿)
|
||||
|
||||
### 2.1 目标
|
||||
- 前端提交批次任务(多模型、多步骤按条目表达)
|
||||
- 后端按模型扩展名路由到对应 CAD 插件
|
||||
- 全局严格 FIFO 串行执行(跨软件也串行)
|
||||
- 插件通过 HTTP 接口执行,结果通过回调闭环
|
||||
- 单任务失败后按重试策略处理,最终失败不阻塞后续任务
|
||||
|
||||
### 2.2 执行策略
|
||||
- 队列:全局单队列 + 单 worker
|
||||
- 顺序:严格按提交顺序
|
||||
- 状态机:`queued -> dispatching -> waiting_callback -> succeeded/failed`
|
||||
- 失败策略:重试后失败,继续执行后续任务
|
||||
- 存储策略:首版内存态(重启不恢复)
|
||||
|
||||
### 2.3 架构模块
|
||||
- 模型层:`app/models/cad_batch.py`
|
||||
- 路由层:`app/core/cad_task_router.py`
|
||||
- 插件下发层:`app/core/plugin_http_client.py`
|
||||
- 回调等待层:`app/core/plugin_callback_registry.py`
|
||||
- 串行执行层:`app/core/serial_batch_executor.py`
|
||||
- 业务管理层:`app/core/cad_batch_manager.py`
|
||||
- 插件回调接口:`app/api/v1/plugin_callbacks.py`
|
||||
- 前端通道(WebSocket):`app/api/v1/websocket.py`
|
||||
|
||||
---
|
||||
|
||||
## 3. 公共接口设计
|
||||
|
||||
### 3.1 WebSocket 消息
|
||||
- `submit_batch_tasks`:提交批次
|
||||
- `get_batch_status`:查询批次
|
||||
- `get_batch_item_status`:查询条目
|
||||
|
||||
### 3.2 WebSocket 推送事件
|
||||
- `batch_created`
|
||||
- `batch_item_update`
|
||||
- `batch_completed`
|
||||
|
||||
### 3.3 插件回调 HTTP
|
||||
- `POST /api/v1/plugin-callbacks/task-result`
|
||||
- 核心字段:
|
||||
- `execution_id`
|
||||
- `software_id`
|
||||
- `status` (`success|failed`)
|
||||
- `error_message`
|
||||
- `result`
|
||||
- `finished_at`
|
||||
- `token/signature`(当前实现为 token 校验)
|
||||
|
||||
---
|
||||
|
||||
## 4. 配置设计
|
||||
|
||||
### 4.1 已支持配置
|
||||
- `routing.extension_to_software`
|
||||
- `plugins.{software_id}.base_url`
|
||||
- `plugins.{software_id}.request_timeout_sec`
|
||||
- `plugins.{software_id}.callback_timeout_sec`
|
||||
- `plugins.{software_id}.max_retries`
|
||||
- `plugins.{software_id}.retry_backoff_sec`
|
||||
- `plugins.{software_id}.callback_token`
|
||||
- `plugins.{software_id}.tasks.{task_type}.path`
|
||||
- `plugins.{software_id}.tasks.{task_type}.body_mode`
|
||||
- `plugins.{software_id}.tasks.{task_type}.completion_mode`
|
||||
|
||||
### 4.2 body_mode 语义
|
||||
- `file_path`:发送 `filePath + task_params + callback元信息`
|
||||
- `task_params_only`:发送 `task_params + callback元信息`
|
||||
- `project_open`:发送 `task_params + callback元信息`
|
||||
- `creo_close_model`:发送 `{ software_type: "creo", force_close: boolean }`
|
||||
- `empty`:发送空请求体 `{}`
|
||||
- `passthrough`:原样透传
|
||||
|
||||
### 4.3 completion_mode 语义
|
||||
- `callback`:下发后进入 `waiting_callback`,等待插件回调判定结果
|
||||
- `sync`:以接口同步返回结果直接判定成功/失败,不等待回调
|
||||
|
||||
---
|
||||
|
||||
## 5. 当前开发进度(截至本次更新)
|
||||
|
||||
### 5.1 已完成
|
||||
- [x] 计划文档落盘:`cad-batch-plan.md`
|
||||
- [x] 批处理核心模型、执行器、管理器已实现
|
||||
- [x] 插件异步回调机制与幂等处理已实现
|
||||
- [x] 主应用 `startup/shutdown` 已接入批处理执行器
|
||||
- [x] WebSocket 提交/查询批次能力已接入
|
||||
- [x] 插件任务映射已支持按 `software_id + task_type` 路由
|
||||
- [x] 单元测试已覆盖路由/执行器/回调/API 映射
|
||||
|
||||
### 5.2 已集成的 CAD 插件资料
|
||||
|
||||
#### Creo (localhost:12345)
|
||||
- `open_model` -> `/api/model/open` (`file_path`)
|
||||
- `shrinkwrap_shell` -> `/api/creo/shrinkwrap/shell` (`task_params_only`)
|
||||
- `shell_analysis` -> `/api/analysis/shell-analysis` (`task_params_only`)
|
||||
- `close_model` -> `/api/model/close` (`creo_close_model`, `sync`, 已接入)
|
||||
|
||||
#### Revit (localhost:9000)
|
||||
- `open_model` -> `/api/open` (`file_path`)
|
||||
- `shell_analyze` -> `/api/shell/analyze` (`task_params_only`)
|
||||
- `shell_execute` -> `/api/shell/execute` (`task_params_only`)
|
||||
- `close_model` -> `/api/close` (`empty`, `sync`, 已接入)
|
||||
|
||||
#### PDMS (localhost:9001)
|
||||
- `open_project` -> `/api/project/open` (`project_open`)
|
||||
- `open_mdb` -> `/api/mdb/open` (`project_open`)
|
||||
- `shrinkwrap_model` -> `/api/model/shrinkwrap` (`task_params_only`)
|
||||
- `simplify_model` -> `/api/model/simplify` (`task_params_only`)
|
||||
- `close_project` -> `/api/project/close` (`task_params_only`, 占位)
|
||||
|
||||
### 5.3 当前测试状态
|
||||
- 已通过:`13 passed`
|
||||
- 覆盖文件:
|
||||
- `tests/test_cad_task_router.py`
|
||||
- `tests/test_serial_batch_executor.py`
|
||||
- `tests/test_plugin_callback_api.py`
|
||||
- `tests/test_plugin_http_client_mapping.py`
|
||||
- `tests/test_creo_close_model_sync_flow.py`
|
||||
|
||||
---
|
||||
|
||||
## 6. 待补资料与后续工作
|
||||
|
||||
### 6.1 你后续补充后可继续对接
|
||||
1. 每个 `task_type` 的最终命名标准(是否沿用当前命名)
|
||||
2. Revit 大任务轮询接口细节(如 `/api/task` 的请求/返回)
|
||||
3. PDMS `close_project` 是否真实可用(当前仍按占位映射)
|
||||
4. 回调安全方案最终版(token 还是 signature,签名算法与字段)
|
||||
5. 各 API 的完整错误码与失败语义(用于统一重试/终止策略)
|
||||
|
||||
### 6.2 下一步建议
|
||||
- 基于真实插件联调一轮 `open -> 处理 -> close`
|
||||
- 优先确认 PDMS `close_project` 实际行为与容错策略
|
||||
- 增加端到端集成测试(含回调超时、插件离线、重复回调场景)
|
||||
|
||||
---
|
||||
|
||||
## 7. 验收标准(当前版本)
|
||||
- 可提交批次并进入全局串行队列
|
||||
- 可按扩展名路由并向插件正确下发
|
||||
- 可通过回调驱动状态闭环
|
||||
- 单任务失败不会阻塞后续任务
|
||||
- 可通过 WebSocket 查询批次与条目状态
|
||||
@ -54,11 +54,38 @@
|
||||
<el-option label="全部" value="all" />
|
||||
<el-option label="运行中" value="running" />
|
||||
<el-option label="排队中" value="queued" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="已完成" value="succeeded" />
|
||||
<el-option label="失败/异常" value="failed" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="filteredJobs" style="width: 100%" stripe>
|
||||
<!-- 展开项:显示具体的子条目 -->
|
||||
<el-table-column type="expand">
|
||||
<template #default="scope">
|
||||
<div style="padding: 10px 40px;">
|
||||
<h4 style="margin-top: 0; margin-bottom: 15px; color: var(--color-text-secondary);">批次明细 (模型条目)</h4>
|
||||
<el-table :data="scope.row.items" size="small" border>
|
||||
<el-table-column prop="modelPath" label="模型路径" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="执行状态" width="150">
|
||||
<template #default="itemScope">
|
||||
<el-tag :type="getItemStatusType(itemScope.row.status)" size="small">
|
||||
{{ getItemStatusLabel(itemScope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="finishedAt" label="完成时间" width="160" />
|
||||
<el-table-column prop="errorMessage" label="错误信息" show-overflow-tooltip>
|
||||
<template #default="itemScope">
|
||||
<span v-if="itemScope.row.errorMessage" style="color: #F56C6C">{{ itemScope.row.errorMessage }}</span>
|
||||
<span v-else class="text-gray-400">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="batchId" label="批次ID" width="120" />
|
||||
<el-table-column prop="strategy" label="执行策略" width="150" />
|
||||
<el-table-column prop="priority" label="优先级" width="100">
|
||||
@ -84,12 +111,11 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button v-if="scope.row.status === 'running'" type="warning" size="small" link @click="pauseJob(scope.row)">暂停</el-button>
|
||||
<el-button v-if="scope.row.status === 'paused'" type="success" size="small" link @click="resumeJob(scope.row)">继续</el-button>
|
||||
<el-button v-if="['running', 'queued', 'paused'].includes(scope.row.status)" type="danger" size="small" link @click="cancelJob(scope.row)">取消</el-button>
|
||||
<el-button v-if="scope.row.status === 'completed'" type="primary" size="small" link @click="viewDetails(scope.row)">详情与报告</el-button>
|
||||
<!-- 取消已支持:如果不是终态则允许取消 -->
|
||||
<el-button v-if="['running', 'queued'].includes(scope.row.status)" type="danger" size="small" link @click="cancelJob(scope.row)">取消批次</el-button>
|
||||
<el-button v-if="['succeeded', 'failed'].includes(scope.row.status)" type="primary" size="small" link @click="viewDetails(scope.row)">详情与报告</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@ -100,18 +126,18 @@
|
||||
<el-dialog v-model="wizardVisible" title="创建批量任务" width="700px" destroy-on-close>
|
||||
<el-steps :active="wizardStep" finish-status="success" align-center class="wizard-steps">
|
||||
<el-step title="选择模型" description="选择要处理的文件/目录"></el-step>
|
||||
<el-step title="配置策略" description="选择分析或导出策略"></el-step>
|
||||
<el-step title="调度设置" description="设置优先级与并发"></el-step>
|
||||
<el-step title="配置策略" description="选择批处理动作指令"></el-step>
|
||||
<el-step title="调度设置" description="设置优先级与排队模式"></el-step>
|
||||
</el-steps>
|
||||
|
||||
<div class="wizard-content">
|
||||
<!-- 步骤1:选择模型 -->
|
||||
<div v-show="wizardStep === 0" class="step-panel">
|
||||
<p class="step-desc">请输入模型路径并点击“增加模型”,构建本次批处理的模型列表:</p>
|
||||
<p class="step-desc">请输入模型路径并点击“增加模型”,构建本次批处理的模型列表(支持 .prt, .rvt 扩展名):</p>
|
||||
<div class="model-path-input-row">
|
||||
<el-input
|
||||
v-model="newModelPath"
|
||||
placeholder="例如:D:\\CAD\\project\\part1.prt"
|
||||
placeholder="例如:C:\CAD\project\part1.prt 或 D:\BIM\model.rvt"
|
||||
@keyup.enter="addModelPath"
|
||||
/>
|
||||
<el-button type="primary" @click="addModelPath">
|
||||
@ -132,15 +158,16 @@
|
||||
<!-- 步骤2:配置策略 -->
|
||||
<div v-show="wizardStep === 1" class="step-panel">
|
||||
<el-form :model="taskForm" label-width="120px">
|
||||
<el-form-item label="执行策略">
|
||||
<el-form-item label="执行策略序列">
|
||||
<el-select v-model="taskForm.strategy" placeholder="请选择策略" style="width: 100%">
|
||||
<el-option label="批量模型轻量化分析 (多算法融合)" value="shrinkwrap_analysis" />
|
||||
<el-option label="批量格式转换 (转STP/中性格式)" value="format_export" />
|
||||
<el-option label="批量层级检索与清理" value="hierarchy_cleanup" />
|
||||
<el-option label="模型打开 -> Shrinkwrap外壳 -> 关闭模型 (Creo)" value="creo_shrinkwrap" />
|
||||
<el-option label="模型打开 -> 建筑模型剥壳 -> 关闭模型 (Revit)" value="revit_shell" />
|
||||
<el-option label="工程打开 -> 模型包裹化防泄密 -> 占位关闭 (PDMS)" value="pdms_shrinkwrap" />
|
||||
<el-option label="工程打开 -> 模型参数化简化 -> 占位关闭 (PDMS)" value="pdms_simplify" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="导出路径">
|
||||
<el-input v-model="taskForm.exportPath" placeholder="输出目录路径 (如: C:\Output)" />
|
||||
<el-form-item label="策略说明">
|
||||
<span class="form-tip" style="margin-left:0;">提交时会将策略转化为相应的 task_type 序述串通过 WebSocket 下发。</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
@ -156,12 +183,8 @@
|
||||
<el-radio label="紧急">紧急</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="最大并发数">
|
||||
<el-input-number v-model="taskForm.maxConcurrency" :min="1" :max="10" />
|
||||
<span class="form-tip">控制同时执行的任务数量</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="计划执行时间">
|
||||
<el-date-picker v-model="taskForm.scheduleAt" type="datetime" placeholder="立即执行可留空" />
|
||||
<el-form-item label="排队模式">
|
||||
<el-tag type="info">全局严格单队列 FIFO 串行执行</el-tag>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
@ -171,7 +194,7 @@
|
||||
<div class="dialog-footer">
|
||||
<el-button v-if="wizardStep > 0" @click="wizardStep--">上一步</el-button>
|
||||
<el-button v-if="wizardStep < 2" type="primary" @click="wizardStep++">下一步</el-button>
|
||||
<el-button v-if="wizardStep === 2" type="success" @click="submitTask">提交任务组</el-button>
|
||||
<el-button v-if="wizardStep === 2" type="success" @click="submitTask">提交调度批次</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -179,7 +202,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
// 模拟状态
|
||||
@ -187,21 +210,58 @@ const statusFilter = ref('all')
|
||||
const wizardVisible = ref(false)
|
||||
const wizardStep = ref(0)
|
||||
const taskForm = ref({
|
||||
strategy: '',
|
||||
exportPath: '',
|
||||
strategy: 'creo_shrinkwrap',
|
||||
priority: '普通',
|
||||
maxConcurrency: 3,
|
||||
scheduleAt: '',
|
||||
modelPaths: []
|
||||
})
|
||||
const newModelPath = ref('')
|
||||
|
||||
// 模拟列表数据
|
||||
// 模拟列表数据 - 配合后端的 queued -> dispatching -> waiting_callback -> succeeded/failed 状态
|
||||
const jobs = ref([
|
||||
{ batchId: 'B-20261001', strategy: '批量格式转换', priority: '普通', progress: 45, status: 'running', totalCount: 10, completedCount: 4, createdAt: '2026-02-26 14:00' },
|
||||
{ batchId: 'B-20261002', strategy: '薄壳化(多算法)', priority: '高', progress: 100, status: 'completed', totalCount: 50, completedCount: 50, createdAt: '2026-02-26 10:30' },
|
||||
{ batchId: 'B-20261003', strategy: '层级清理', priority: '低', progress: 0, status: 'queued', totalCount: 25, completedCount: 0, createdAt: '2026-02-26 15:20' },
|
||||
{ batchId: 'B-20261004', strategy: '批量格式转换', priority: '紧急', progress: 80, status: 'paused', totalCount: 100, completedCount: 80, createdAt: '2026-02-25 09:15' }
|
||||
{
|
||||
batchId: 'B-20261001',
|
||||
strategy: 'creo_shrinkwrap',
|
||||
priority: '普通',
|
||||
progress: 40,
|
||||
status: 'running',
|
||||
totalCount: 5,
|
||||
completedCount: 2,
|
||||
createdAt: '2026-02-26 14:00',
|
||||
items: [
|
||||
{ modelPath: 'C:\\Models\\engine_block.prt', status: 'succeeded', errorMessage: '', finishedAt: '2026-02-26 14:05' },
|
||||
{ modelPath: 'C:\\Models\\gear_box.prt', status: 'failed', errorMessage: '模型几何损坏无法抽取外壳', finishedAt: '2026-02-26 14:10' },
|
||||
{ modelPath: 'C:\\Models\\shaft.prt', status: 'waiting_callback', errorMessage: '', finishedAt: '-' },
|
||||
{ modelPath: 'C:\\Models\\bearing.prt', status: 'queued', errorMessage: '', finishedAt: '-' },
|
||||
{ modelPath: 'C:\\Models\\casing.prt', status: 'queued', errorMessage: '', finishedAt: '-' }
|
||||
]
|
||||
},
|
||||
{
|
||||
batchId: 'B-20261002',
|
||||
strategy: 'revit_shell',
|
||||
priority: '高',
|
||||
progress: 100,
|
||||
status: 'succeeded',
|
||||
totalCount: 2,
|
||||
completedCount: 2,
|
||||
createdAt: '2026-02-26 10:30',
|
||||
items: [
|
||||
{ modelPath: 'D:\\BIM\\A_Tower.rvt', status: 'succeeded', errorMessage: '', finishedAt: '2026-02-26 10:45' },
|
||||
{ modelPath: 'D:\\BIM\\B_Tower.rvt', status: 'succeeded', errorMessage: '', finishedAt: '2026-02-26 11:00' }
|
||||
]
|
||||
},
|
||||
{
|
||||
batchId: 'B-20261003',
|
||||
strategy: 'pdms_shrinkwrap',
|
||||
priority: '低',
|
||||
progress: 0,
|
||||
status: 'queued',
|
||||
totalCount: 1,
|
||||
completedCount: 0,
|
||||
createdAt: '2026-02-26 15:20',
|
||||
items: [
|
||||
{ modelPath: 'SAM/CATA', status: 'queued', errorMessage: '', finishedAt: '-' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const filteredJobs = computed(() => {
|
||||
@ -209,15 +269,22 @@ const filteredJobs = computed(() => {
|
||||
return jobs.value.filter(j => j.status === statusFilter.value)
|
||||
})
|
||||
|
||||
// === WebSocket 模拟与生命周期 ===
|
||||
// 这里为后续真实接入 WebSocket 通道(submit_batch_tasks / batch_item_update)留出框架
|
||||
onMounted(() => {
|
||||
// console.log('连接 WebSocket 后端,监听 batch_created, batch_item_update, batch_completed 事件')
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// console.log('断开 WebSocket')
|
||||
})
|
||||
|
||||
// 向导控制
|
||||
const openWizard = () => {
|
||||
wizardStep.value = 0
|
||||
taskForm.value = {
|
||||
strategy: '',
|
||||
exportPath: '',
|
||||
strategy: 'creo_shrinkwrap',
|
||||
priority: '普通',
|
||||
maxConcurrency: 3,
|
||||
scheduleAt: '',
|
||||
modelPaths: []
|
||||
}
|
||||
wizardVisible.value = true
|
||||
@ -230,24 +297,46 @@ const submitTask = () => {
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.success('批量任务已成功加入调度队列!')
|
||||
// 模拟:组装 submit_batch_tasks WebSocket Request 发送给后端
|
||||
// const payload = { ... }
|
||||
// ws.send(JSON.stringify({ type: 'submit_batch_tasks', payload }))
|
||||
|
||||
ElMessage.success('批量任务已成功提交至全局串行队列!')
|
||||
wizardVisible.value = false
|
||||
// 将新任务加入 mock 数据 (实际应调用后端的 create job 接口)
|
||||
|
||||
// 将新任务加入 mock 数据模拟事件推送 batch_created
|
||||
jobs.value.unshift({
|
||||
batchId: `B-${Date.now().toString().slice(-8)}`,
|
||||
strategy: taskForm.value.strategy === 'format_export' ? '批量格式转换' : '综合分析',
|
||||
strategy: taskForm.value.strategy,
|
||||
priority: taskForm.value.priority,
|
||||
progress: 0,
|
||||
status: 'queued',
|
||||
totalCount: taskForm.value.modelPaths.length,
|
||||
completedCount: 0,
|
||||
createdAt: new Date().toLocaleString()
|
||||
createdAt: new Date().toLocaleString(),
|
||||
items: taskForm.value.modelPaths.map(p => ({
|
||||
modelPath: p,
|
||||
status: 'queued',
|
||||
errorMessage: '',
|
||||
finishedAt: '-'
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
const addModelPath = () => {
|
||||
const path = newModelPath.value.trim()
|
||||
if (!path) return
|
||||
|
||||
// 按照路由规范,增加简单的扩展名限制校验
|
||||
const lowerPath = path.toLowerCase();
|
||||
const validExtensions = ['.prt', '.asm', '.rvt', '.rfa', '.ifc', 'sam', 'mdb']
|
||||
const isValid = validExtensions.some(ext => lowerPath.endsWith(ext) || lowerPath.includes(ext))
|
||||
|
||||
if (!isValid && !taskForm.value.strategy.includes('pdms')) {
|
||||
ElMessage.warning('不支持的扩展名。目前支持的路由扩展名例如 .prt, .rvt')
|
||||
// 实际生产中可以允许强制添加,或者更精准校验,这里模拟强限制
|
||||
}
|
||||
|
||||
if (taskForm.value.modelPaths.includes(path)) {
|
||||
ElMessage.warning('该模型路径已存在')
|
||||
return
|
||||
@ -262,11 +351,11 @@ const removeModelPath = (index) => {
|
||||
|
||||
// 格式辅助
|
||||
const getStatusLabel = (status) => {
|
||||
const map = { running: '执行中', completed: '已完成', queued: '排队中', paused: '已暂停', failed: '异常' }
|
||||
const map = { running: '执行中', succeeded: '已完成', queued: '排队中', failed: '失败/异常' }
|
||||
return map[status] || status
|
||||
}
|
||||
const getStatusType = (status) => {
|
||||
const map = { running: 'primary', completed: 'success', queued: 'info', paused: 'warning', failed: 'danger' }
|
||||
const map = { running: 'primary', succeeded: 'success', queued: 'info', failed: 'danger' }
|
||||
return map[status] || 'info'
|
||||
}
|
||||
const getPriorityType = (priority) => {
|
||||
@ -274,23 +363,43 @@ const getPriorityType = (priority) => {
|
||||
return map[priority] || 'info'
|
||||
}
|
||||
const getProgressStatus = (status) => {
|
||||
if (status === 'completed') return 'success'
|
||||
if (status === 'succeeded') return 'success'
|
||||
if (status === 'failed') return 'exception'
|
||||
if (status === 'paused') return 'warning'
|
||||
return ''
|
||||
return '' /* running/queued 无特殊状态效果 */
|
||||
}
|
||||
|
||||
// 操作事件 (模拟控制API)
|
||||
const pauseJob = (row) => { row.status = 'paused' }
|
||||
const resumeJob = (row) => { row.status = 'running' }
|
||||
// 子条目格式辅助
|
||||
const getItemStatusLabel = (status) => {
|
||||
const map = {
|
||||
queued: '排队中',
|
||||
dispatching: '正在下发',
|
||||
waiting_callback: '执行中(等待回调)',
|
||||
succeeded: '成功',
|
||||
failed: '失败'
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
const getItemStatusType = (status) => {
|
||||
const map = {
|
||||
queued: 'info',
|
||||
dispatching: 'primary',
|
||||
waiting_callback: 'warning',
|
||||
succeeded: 'success',
|
||||
failed: 'danger'
|
||||
}
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
// 操作事件
|
||||
const cancelJob = (row) => {
|
||||
ElMessageBox.confirm('确定取消该批量任务?', '警告', { type: 'warning' }).then(() => {
|
||||
ElMessageBox.confirm('确定取消该批次任务? 正在执行的条目可能无法撤回。', '警告', { type: 'warning' }).then(() => {
|
||||
// 模拟从列表移除,实际应通过 ws 接口发送取消批次指令
|
||||
jobs.value = jobs.value.filter(j => j.batchId !== row.batchId)
|
||||
ElMessage.success('任务已取消')
|
||||
ElMessage.success('批次任务已取消')
|
||||
}).catch(()=> {})
|
||||
}
|
||||
const viewDetails = (row) => {
|
||||
ElMessage.info(`跳转至报告视图,查看 ${row.batchId} 详情日志`)
|
||||
ElMessage.info(`跳转至报告视图,查看 ${row.batchId} 详情日志及完整失败日志`)
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -488,6 +597,11 @@ const viewDetails = (row) => {
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
/* 展开行内容区域背景 */
|
||||
:deep(.el-table__expanded-cell) {
|
||||
background-color: rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
/* 固定列默认会有独立背景,保持与行背景一致,避免发白 */
|
||||
:deep(.el-table .el-table-fixed-column--left),
|
||||
:deep(.el-table .el-table-fixed-column--right) {
|
||||
|
||||
@ -31,6 +31,14 @@
|
||||
<i class="fas fa-building"></i>
|
||||
<span>IFC 转 STP</span>
|
||||
</div>
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: currentMode === 'ifc-to-stp-batch' }"
|
||||
@click="currentMode = 'ifc-to-stp-batch'"
|
||||
>
|
||||
<i class="fas fa-layer-group"></i>
|
||||
<span>IFC 批量转 STP</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="conversion-flow">
|
||||
@ -39,9 +47,11 @@
|
||||
<i class="fas fa-file-upload"></i>
|
||||
</div>
|
||||
<h3 v-if="currentMode === 'stp-to-gltf'">1. 选择STP文件</h3>
|
||||
<h3 v-else>1. 选择IFC文件</h3>
|
||||
<h3 v-else-if="currentMode === 'ifc-to-stp'">1. 选择IFC文件</h3>
|
||||
<h3 v-else>1. 选择多个IFC</h3>
|
||||
<p v-if="currentMode === 'stp-to-gltf'">选择要转换的STEP格式文件</p>
|
||||
<p v-else>选择要转换的IFC格式文件</p>
|
||||
<p v-else-if="currentMode === 'ifc-to-stp'">选择要转换的IFC格式文件</p>
|
||||
<p v-else>输入多个要转换的IFC格式文件</p>
|
||||
</div>
|
||||
<div class="flow-arrow">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
@ -52,7 +62,8 @@
|
||||
</div>
|
||||
<h3>2. 转换处理</h3>
|
||||
<p v-if="currentMode === 'stp-to-gltf'">自动处理几何数据和材质</p>
|
||||
<p v-else>解析BIM数据并重建BREP几何</p>
|
||||
<p v-else-if="currentMode === 'ifc-to-stp'">解析BIM数据并重建BREP几何</p>
|
||||
<p v-else>批量解析BIM数据并重建BREP几何</p>
|
||||
</div>
|
||||
<div class="flow-arrow">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
@ -62,9 +73,11 @@
|
||||
<i class="fas fa-download"></i>
|
||||
</div>
|
||||
<h3 v-if="currentMode === 'stp-to-gltf'">3. 下载GLTF</h3>
|
||||
<h3 v-else>3. 下载STP</h3>
|
||||
<h3 v-else-if="currentMode === 'ifc-to-stp'">3. 获取STP</h3>
|
||||
<h3 v-else>3. 获取多个STP</h3>
|
||||
<p v-if="currentMode === 'stp-to-gltf'">获取Web优化的3D模型文件</p>
|
||||
<p v-else>获取通用的STEP实体模型文件</p>
|
||||
<p v-else-if="currentMode === 'ifc-to-stp'">获取通用的STEP实体模型文件</p>
|
||||
<p v-else>获取批量的STEP实体模型文件</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -82,7 +95,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="path-input-section" v-else>
|
||||
<div class="path-input-section" v-else-if="currentMode === 'ifc-to-stp'">
|
||||
<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">
|
||||
@ -93,6 +106,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="batch-input-section" v-else-if="currentMode === 'ifc-to-stp-batch'">
|
||||
<div class="batch-header">
|
||||
<h4><i class="fas fa-list-ol"></i> 批量转换列表</h4>
|
||||
<button class="add-batch-btn" @click="addBatchItem">
|
||||
<i class="fas fa-plus"></i> 添加项
|
||||
</button>
|
||||
</div>
|
||||
<div class="batch-list">
|
||||
<div class="batch-item" v-for="(item, index) in batchItems" :key="index">
|
||||
<div class="batch-index">{{ index + 1 }}</div>
|
||||
<div class="batch-inputs">
|
||||
<div class="batch-input-field">
|
||||
<span class="batch-label">IFC 路径:</span>
|
||||
<input type="text" v-model="item.ifcPath" placeholder="例如:D:/CAD/input1.ifc" class="path-input batch-input">
|
||||
</div>
|
||||
<div class="batch-input-field">
|
||||
<span class="batch-label">STP 路径:</span>
|
||||
<input type="text" v-model="item.stpPath" placeholder="例如:D:/CAD/output1.stp" class="path-input batch-input">
|
||||
</div>
|
||||
</div>
|
||||
<button class="remove-batch-btn" @click="removeBatchItem(index)" :disabled="batchItems.length <= 1" title="移除该项">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="conversion-options">
|
||||
<h4>转换选项</h4>
|
||||
<!-- STP to GLTF Options -->
|
||||
@ -127,6 +167,12 @@
|
||||
|
||||
<!-- IFC to STP Options -->
|
||||
<div class="options-grid" v-else>
|
||||
<div class="option-item" v-if="currentMode === 'ifc-to-stp-batch'">
|
||||
<label>
|
||||
<input type="checkbox" v-model="continueOnError">
|
||||
出错时继续转换其余项
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<label>STEP版本:</label>
|
||||
<select id="stp-version">
|
||||
@ -195,6 +241,24 @@ const currentMode = ref('stp-to-gltf')
|
||||
// IFC to STP 转换相关状态
|
||||
const ifcPath = ref('')
|
||||
const stpPath = ref('')
|
||||
|
||||
// IFC 批量转 STP 状态
|
||||
const batchItems = ref([
|
||||
{ ifcPath: '', stpPath: '' },
|
||||
{ ifcPath: '', stpPath: '' }
|
||||
])
|
||||
const continueOnError = ref(true)
|
||||
|
||||
const addBatchItem = () => {
|
||||
batchItems.value.push({ ifcPath: '', stpPath: '' })
|
||||
}
|
||||
|
||||
const removeBatchItem = (index) => {
|
||||
if (batchItems.value.length > 1) {
|
||||
batchItems.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const isConverting = ref(false)
|
||||
const convertResult = ref('') // 'success', 'error', ''
|
||||
const progressTitle = ref('准备转换...')
|
||||
@ -203,6 +267,8 @@ const progressStatus = ref('')
|
||||
const canConvert = computed(() => {
|
||||
if (currentMode.value === 'ifc-to-stp') {
|
||||
return ifcPath.value.trim() !== '' && stpPath.value.trim() !== ''
|
||||
} else if (currentMode.value === 'ifc-to-stp-batch') {
|
||||
return batchItems.value.length > 0 && batchItems.value.every(item => item.ifcPath.trim() !== '' && item.stpPath.trim() !== '')
|
||||
}
|
||||
return false // STP to GLTF 目前尚未实现功能
|
||||
})
|
||||
@ -214,6 +280,11 @@ const selectFile = () => {
|
||||
const resetForm = () => {
|
||||
ifcPath.value = ''
|
||||
stpPath.value = ''
|
||||
batchItems.value = [
|
||||
{ ifcPath: '', stpPath: '' },
|
||||
{ ifcPath: '', stpPath: '' }
|
||||
]
|
||||
continueOnError.value = true
|
||||
convertResult.value = ''
|
||||
progressTitle.value = '准备转换...'
|
||||
progressStatus.value = ''
|
||||
@ -231,22 +302,48 @@ const startConversion = () => {
|
||||
ifc_path: ifcPath.value.trim(),
|
||||
stp_path: stpPath.value.trim()
|
||||
})
|
||||
} else if (currentMode.value === 'ifc-to-stp-batch') {
|
||||
isConverting.value = true
|
||||
convertResult.value = ''
|
||||
progressTitle.value = '正在批量转换...'
|
||||
progressStatus.value = `已发送 ${batchItems.value.length} 个文件的请求,处理中...`
|
||||
|
||||
websocketService.send({
|
||||
type: "convert_ifc_to_stp_batch",
|
||||
continue_on_error: continueOnError.value,
|
||||
items: batchItems.value.map(item => ({
|
||||
ifc_path: item.ifcPath.trim(),
|
||||
stp_path: item.stpPath.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 === "info"
|
||||
if (message.type === "info") {
|
||||
if (message.data?.items && Array.isArray(message.data.items)) {
|
||||
isConverting.value = false
|
||||
convertResult.value = 'success'
|
||||
progressTitle.value = '批量转换完成!'
|
||||
let successCount = message.data.items.filter(i => i.status === 'success' || i.success).length || batchItems.value.length
|
||||
progressStatus.value = `处理完成收工 (成功: ${successCount}项, 耗时: ${message.data.duration_ms || '?'}ms)`
|
||||
} else if (message.data?.stp_path) {
|
||||
isConverting.value = false
|
||||
convertResult.value = 'success'
|
||||
progressTitle.value = '转换成功!'
|
||||
progressStatus.value = `文件已保存至: ${message.data.stp_path} (耗时: ${message.data.duration_ms}ms)`
|
||||
} else {
|
||||
isConverting.value = false
|
||||
convertResult.value = 'success'
|
||||
progressTitle.value = '转换完成!'
|
||||
progressStatus.value = `处理完成收工 (耗时: ${message.data?.duration_ms || '?'}ms)`
|
||||
}
|
||||
}
|
||||
// 失败看 msg.type === "error"
|
||||
else if (message.type === "error") {
|
||||
// 假设错误跟转换请求相关,这里做一个简单匹配或是只要是在转换期间遇到error就认为是失败
|
||||
isConverting.value = false
|
||||
convertResult.value = 'error'
|
||||
progressTitle.value = '转换失败'
|
||||
@ -633,6 +730,145 @@ onUnmounted(() => {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 批量输入样式 */
|
||||
.batch-input-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
background: var(--color-bg-tertiary);
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.batch-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.batch-header h4 {
|
||||
color: var(--color-text-primary);
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.batch-header h4 i {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.add-batch-btn {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px dashed var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.add-batch-btn:hover {
|
||||
background: var(--color-primary-rgb-2);
|
||||
}
|
||||
|
||||
.batch-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.batch-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.batch-list::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border-primary);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.batch-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: var(--color-bg-secondary);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
.batch-index {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary-rgb-2);
|
||||
color: var(--color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.batch-inputs {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.batch-input-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.batch-label {
|
||||
width: 70px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.batch-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.remove-batch-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--color-danger);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.remove-batch-btn:hover:not(:disabled) {
|
||||
background: rgba(245, 108, 108, 0.1);
|
||||
}
|
||||
|
||||
.remove-batch-btn:disabled {
|
||||
color: var(--color-text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 路径输入样式 */
|
||||
.path-input-section {
|
||||
display: flex;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user