CAD-BIM 工程量提取 HTML 报告生成器
高级
这是一个Engineering领域的自动化工作流,包含 18 个节点。主要使用 If, Set, Function, ManualTrigger, ExecuteCommand 等节点。 从Revit模型生成交互式墙体工程量HTML报告
前置要求
- •无特殊前置要求,导入即可使用
使用的节点 (18)
分类
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
"id": "Hta5FU7DD54QKxUK",
"meta": {
"instanceId": "5ba872643a08525d217680a9bbff49bd9855bd3fe8ccba4458b363643e613bee"
},
"name": "CAD-BIM 工程量提取 HTML 报告生成器",
"tags": [],
"nodes": [
{
"id": "fe089e9a-bb16-440b-900d-21c5cf782286",
"name": "开始 - 点击启动",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-1780,
580
],
"parameters": {},
"typeVersion": 1
},
{
"id": "e56f9ca7-214a-4c90-a6fa-657d6c875e26",
"name": "设置 - 定义文件路径",
"type": "n8n-nodes-base.set",
"position": [
-1580,
580
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "9cbd4ec9-df24-41e8-b47a-720a4cdb733b",
"name": "path_to_revit_converter",
"type": "string",
"value": "C:\\DDC\\DDC_Converter\\Release\\Community\\DDC_Converter_Revit_Community\\DDC_Converter_Revit_Community\\RvtExporter.exe"
},
{
"id": "aa834467-80fb-476a-bac1-6728478834f0",
"name": "revit_file",
"type": "string",
"value": "C:\\DDC\\DDC_Converter\\Release\\Community\\DDC_Converter_Revit_Community\\RVT_sample\\2023 racbasicsampleproject.rvt"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "5493ccd6-3126-4a7a-a2fa-ca286a69935b",
"name": "提取 - 运行 Revit 转换器",
"type": "n8n-nodes-base.executeCommand",
"position": [
-1380,
580
],
"parameters": {
"command": "=\"{{$json[\"path_to_revit_converter\"]}}\" \"{{$json[\"revit_file\"]}}\""
},
"typeVersion": 1,
"continueOnFail": true
},
{
"id": "433af16e-b5b4-4b02-989b-7c140afa52b7",
"name": "检查 - 提取是否成功?",
"type": "n8n-nodes-base.if",
"position": [
-1180,
580
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "condition1",
"operator": {
"type": "object",
"operation": "exists",
"rightType": "any"
},
"leftValue": "={{ $node[\"Extract - Run Revit converter\"].json.error }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2
},
{
"id": "de432702-44fb-4cf0-90f0-78fb50206721",
"name": "成功 - 创建 Excel 文件名",
"type": "n8n-nodes-base.set",
"position": [
-980,
580
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "9cbd4ec9-df24-41e8-b47a-720a4cdb733b",
"name": "xlsx_filename",
"type": "string",
"value": "={{ $node[\"Setup - Define file paths\"].json[\"revit_file\"].slice(0, -4) + \"_rvt.xlsx\" }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "419aaa9e-7edc-43b4-a666-2700de939884",
"name": "错误 - 显示问题详情",
"type": "n8n-nodes-base.set",
"position": [
-980,
380
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "error-message-id",
"name": "error_message",
"type": "string",
"value": "=Extraction failed: {{ $node[\"Extract - Run Revit converter\"].json.error || \"Unknown error\" }}"
},
{
"id": "error-code-id",
"name": "error_code",
"type": "number",
"value": "={{ $node[\"Extract - Run Revit converter\"].json.code || -1 }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "673bbba9-1537-4852-8d4e-b67c4991253c",
"name": "提取 - 从磁盘读取 Excel 文件",
"type": "n8n-nodes-base.readBinaryFile",
"position": [
-780,
580
],
"parameters": {
"filePath": "={{ $json[\"xlsx_filename\"] }}"
},
"typeVersion": 1
},
{
"id": "c4b05b5b-6d0b-4ab8-89fb-a54fe86899d9",
"name": "提取 - 将 Excel 解析为数据",
"type": "n8n-nodes-base.spreadsheetFile",
"position": [
-580,
580
],
"parameters": {
"options": {}
},
"typeVersion": 1
},
{
"id": "5d2bdce5-75d2-40a2-b10b-fd443a982118",
"name": "转换 - 仅筛选 OST_Walls",
"type": "n8n-nodes-base.if",
"position": [
-380,
580
],
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json[\"Category : String\"] }}",
"value2": "OST_Walls"
}
]
}
},
"typeVersion": 1
},
{
"id": "5c2ac481-8340-4221-afdb-a3c7ca520abb",
"name": "转换 - 清理墙体数据",
"type": "n8n-nodes-base.set",
"position": [
-180,
580
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "wall-type-name",
"name": "type_name",
"type": "string",
"value": "={{ $json[\"Type Name : String\"] || \"Unknown Type\" }}"
},
{
"id": "wall-volume",
"name": "volume",
"type": "number",
"value": "={{ parseFloat($json[\"Volume : Double\"]) || 0 }}"
},
{
"id": "processed-date",
"name": "processed_date",
"type": "string",
"value": "={{ new Date().toISOString() }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "74b0b328-820d-4f85-bc8a-c6bb6d983d97",
"name": "转换 - 按类型名称分组并汇总体积",
"type": "n8n-nodes-base.function",
"position": [
40,
580
],
"parameters": {
"functionCode": "// Group walls by Type Name and sum Volume\n// Using only built-in JavaScript functions\n\n// Get all wall elements\nconst allWalls = $input.all().map(item => item.json);\n\n// Group by type_name using reduce\nconst grouped = allWalls.reduce((acc, wall) => {\n const typeName = wall.type_name;\n if (!acc[typeName]) {\n acc[typeName] = [];\n }\n acc[typeName].push(wall);\n return acc;\n}, {});\n\n// Create grouping results\nconst groupedResults = Object.keys(grouped).map(typeName => {\n const walls = grouped[typeName];\n const totalVolume = walls.reduce((sum, wall) => sum + (wall.volume || 0), 0);\n const count = walls.length;\n \n return {\n type_name: typeName,\n wall_count: count,\n total_volume: Math.round(totalVolume * 1000) / 1000, // round to 3 decimal places\n average_volume: count > 0 ? Math.round((totalVolume / count) * 1000) / 1000 : 0,\n processed_date: new Date().toISOString()\n };\n});\n\n// Sort by total volume descending\nconst sortedResults = groupedResults.sort((a, b) => b.total_volume - a.total_volume);\n\nreturn sortedResults.map(item => ({ json: item }));"
},
"typeVersion": 1
},
{
"id": "1ab073ee-de46-4224-a4b1-cee2e92dfe20",
"name": "转换 - 生成 HTML 报告",
"type": "n8n-nodes-base.function",
"position": [
240,
580
],
"parameters": {
"functionCode": "// Generate HTML report from grouped wall data\nconst groupedData = $input.all().map(item => item.json);\n\n// Create summary statistics\nconst totalWallTypes = groupedData.length;\nconst totalWalls = groupedData.reduce((sum, item) => sum + item.wall_count, 0);\nconst totalVolume = groupedData.reduce((sum, item) => sum + item.total_volume, 0);\nconst avgVolumePerWall = totalWalls > 0 ? Math.round((totalVolume / totalWalls) * 1000) / 1000 : 0;\n\nconst sourceFile = $node[\"Setup - Define file paths\"].json[\"revit_file\"];\nconst processedDate = new Date().toLocaleString();\n\n// Generate wall type cards HTML\nconst wallTypeCards = groupedData.map((wallType, index) => {\n const percentage = totalVolume > 0 ? (wallType.total_volume / totalVolume) * 100 : 0;\n return `\n <div class=\"wall-type-card\" style=\"animation-delay: ${index * 0.1}s;\">\n <div class=\"wall-type-header\">\n <div class=\"wall-type-name\">${wallType.type_name}</div>\n <div class=\"wall-count\">${wallType.wall_count} walls</div>\n </div>\n <div class=\"wall-metrics\">\n <div class=\"metric\">\n <div class=\"metric-label\">Total Volume</div>\n <div class=\"metric-value\">${wallType.total_volume.toFixed(3)} m³</div>\n </div>\n <div class=\"metric\">\n <div class=\"metric-label\">Average Volume</div>\n <div class=\"metric-value\">${wallType.average_volume.toFixed(3)} m³</div>\n </div>\n <div class=\"metric\">\n <div class=\"metric-label\">Percentage</div>\n <div class=\"metric-value\">${percentage.toFixed(1)}%</div>\n </div>\n </div>\n <div class=\"volume-bar\">\n <div class=\"volume-fill\" style=\"width: ${percentage}%;\"></div>\n </div>\n </div>`;\n}).join('');\n\n// Generate complete HTML\nconst htmlContent = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Quantity Take Off Report</title>\n <style>\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n body {\n font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n min-height: 100vh;\n padding: 20px;\n }\n\n .container {\n max-width: 1000px;\n margin: 0 auto;\n background: rgba(255, 255, 255, 0.95);\n border-radius: 20px;\n box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);\n overflow: hidden;\n backdrop-filter: blur(10px);\n }\n\n .header {\n background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);\n color: white;\n padding: 30px;\n text-align: center;\n }\n\n .header h1 {\n font-size: 2.2rem;\n margin-bottom: 8px;\n }\n\n .header p {\n font-size: 1rem;\n opacity: 0.9;\n }\n\n .summary-cards {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 15px;\n padding: 30px;\n background: #f8f9fa;\n }\n\n .summary-card {\n background: white;\n padding: 20px;\n border-radius: 12px;\n text-align: center;\n box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);\n transition: transform 0.3s ease;\n border-left: 4px solid;\n }\n\n .summary-card:hover {\n transform: translateY(-3px);\n }\n\n .summary-card.types { border-left-color: #e74c3c; }\n .summary-card.walls { border-left-color: #3498db; }\n .summary-card.volume { border-left-color: #2ecc71; }\n .summary-card.average { border-left-color: #f39c12; }\n\n .summary-card h3 {\n color: #2c3e50;\n font-size: 0.8rem;\n text-transform: uppercase;\n letter-spacing: 1px;\n margin-bottom: 8px;\n }\n\n .summary-card .value {\n font-size: 1.8rem;\n font-weight: bold;\n color: #2c3e50;\n margin-bottom: 4px;\n }\n\n .summary-card .unit {\n color: #7f8c8d;\n font-size: 0.8rem;\n }\n\n .content {\n padding: 30px;\n }\n\n .section-title {\n font-size: 1.5rem;\n color: #2c3e50;\n margin-bottom: 20px;\n text-align: center;\n position: relative;\n }\n\n .section-title::after {\n content: '';\n position: absolute;\n bottom: -8px;\n left: 50%;\n transform: translateX(-50%);\n width: 60px;\n height: 3px;\n background: linear-gradient(135deg, #3498db, #e74c3c);\n border-radius: 2px;\n }\n\n .wall-types-grid {\n display: grid;\n gap: 15px;\n margin-top: 20px;\n }\n\n .wall-type-card {\n background: white;\n border-radius: 12px;\n padding: 20px;\n box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1);\n transition: all 0.3s ease;\n border-left: 4px solid #3498db;\n }\n\n .wall-type-card:hover {\n transform: translateY(-2px);\n box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);\n }\n\n .wall-type-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 15px;\n }\n\n .wall-type-name {\n font-size: 1.1rem;\n font-weight: bold;\n color: #2c3e50;\n }\n\n .wall-count {\n background: linear-gradient(135deg, #3498db, #2980b9);\n color: white;\n padding: 6px 12px;\n border-radius: 15px;\n font-size: 0.8rem;\n font-weight: bold;\n }\n\n .wall-metrics {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));\n gap: 10px;\n }\n\n .metric {\n text-align: center;\n padding: 12px;\n background: #f8f9fa;\n border-radius: 8px;\n }\n\n .metric-label {\n font-size: 0.7rem;\n color: #6c757d;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n margin-bottom: 4px;\n }\n\n .metric-value {\n font-size: 1rem;\n font-weight: bold;\n color: #2c3e50;\n }\n\n .volume-bar {\n width: 100%;\n height: 6px;\n background: #e9ecef;\n border-radius: 3px;\n margin-top: 12px;\n overflow: hidden;\n }\n\n .volume-fill {\n height: 100%;\n background: linear-gradient(90deg, #3498db, #2ecc71);\n border-radius: 3px;\n transition: width 0.8s ease;\n }\n\n .footer {\n background: #2c3e50;\n color: white;\n padding: 20px;\n text-align: center;\n font-size: 0.9rem;\n }\n\n .footer-info {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 15px;\n margin-bottom: 15px;\n }\n\n .footer-item strong {\n display: block;\n margin-bottom: 4px;\n color: #3498db;\n }\n\n @media (max-width: 768px) {\n .header h1 { font-size: 1.8rem; }\n .summary-cards { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); padding: 20px; }\n .content { padding: 20px; }\n .wall-type-header { flex-direction: column; align-items: flex-start; gap: 8px; }\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <h1>📊 Quantity Take Off Report</h1>\n <p>Wall analysis and volume calculations from Revit data</p>\n </div>\n\n <div class=\"summary-cards\">\n <div class=\"summary-card types\">\n <h3>Wall Types</h3>\n <div class=\"value\">${totalWallTypes}</div>\n <div class=\"unit\">Different Types</div>\n </div>\n <div class=\"summary-card walls\">\n <h3>Total Walls</h3>\n <div class=\"value\">${totalWalls}</div>\n <div class=\"unit\">Wall Elements</div>\n </div>\n <div class=\"summary-card volume\">\n <h3>Total Volume</h3>\n <div class=\"value\">${totalVolume.toFixed(3)}</div>\n <div class=\"unit\">m³</div>\n </div>\n <div class=\"summary-card average\">\n <h3>Average Volume</h3>\n <div class=\"value\">${avgVolumePerWall.toFixed(3)}</div>\n <div class=\"unit\">m³ per wall</div>\n </div>\n </div>\n\n <div class=\"content\">\n <h2 class=\"section-title\">Wall Types Breakdown</h2>\n <div class=\"wall-types-grid\">\n ${wallTypeCards}\n </div>\n </div>\n\n <div class=\"footer\">\n <div class=\"footer-info\">\n <div class=\"footer-item\">\n <strong>Source File:</strong>\n ${sourceFile.split('\\\\').pop()}\n </div>\n <div class=\"footer-item\">\n <strong>Processed Date:</strong>\n ${processedDate}\n </div>\n <div class=\"footer-item\">\n <strong>Generated By:</strong>\n n8n Workflow Pipeline\n </div>\n </div>\n <p style=\"margin-top: 15px; opacity: 0.7;\">\n Data extracted from Revit and processed through automated ETL pipeline.\n </p>\n </div>\n </div>\n</body>\n</html>`;\n\n// Create filename based on source file\nconst sourceBasename = sourceFile.replace(/\\.[^/.]+$/, \"\"); // Remove extension\nconst outputFilename = sourceBasename + \"_quantity_takeoff_report.html\";\n\n// Create proper binary data structure for n8n writeBinaryFile node\nconst binaryData = {\n data: Buffer.from(htmlContent, 'utf8'),\n mimeType: 'text/html',\n fileName: outputFilename.split('\\\\').pop() // Get just filename without path\n};\n\n// Return data with proper binary structure\nreturn [{\n json: {\n filename: outputFilename,\n summary: {\n total_wall_types: totalWallTypes,\n total_walls: totalWalls,\n total_volume: Math.round(totalVolume * 1000) / 1000,\n average_volume_per_wall: avgVolumePerWall,\n processed_at: new Date().toISOString(),\n source_file: sourceFile\n },\n grouped_data: groupedData,\n message: `Generated HTML report for ${totalWallTypes} wall types (${totalWalls} total walls)`,\n htmlContent: htmlContent\n },\n binary: {\n data: binaryData\n }\n}];"
},
"typeVersion": 1
},
{
"id": "a33f33de-dc6d-4fe7-bf27-86156906827b",
"name": "加载 - 保存 HTML 报告",
"type": "n8n-nodes-base.writeBinaryFile",
"position": [
460,
580
],
"parameters": {
"options": {},
"fileName": "={{ $json.filename }}"
},
"typeVersion": 1
},
{
"id": "feb89705-054c-446a-940b-60ba849fcdfe",
"name": "成功 - 最终结果",
"type": "n8n-nodes-base.set",
"position": [
660,
580
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "success-message",
"name": "success",
"type": "boolean",
"value": true
},
{
"id": "final-message",
"name": "final_message",
"type": "string",
"value": "={{ $node[\"Transform - Generate HTML Report\"].json.message }}. File saved as: {{ $node[\"Transform - Generate HTML Report\"].json.filename }}"
},
{
"id": "summary-data",
"name": "summary",
"type": "object",
"value": "={{ $node[\"Transform - Generate HTML Report\"].json.summary }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "d7749dcd-349f-455c-aeb6-3cbfeccaf26b",
"name": "提取阶段说明",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1840,
240
],
"parameters": {
"color": 6,
"width": 1400,
"height": 520,
"content": "## 🔷 提取阶段"
},
"typeVersion": 1
},
{
"id": "eaa62f9b-20cf-4aa6-afd9-c51dab8d3dd4",
"name": "转换阶段说明",
"type": "n8n-nodes-base.stickyNote",
"position": [
-420,
240
],
"parameters": {
"color": 4,
"width": 800,
"height": 520,
"content": "## 🔷 转换阶段"
},
"typeVersion": 1
},
{
"id": "fb894369-4cb2-42a2-abf8-e0c61ba8493c",
"name": "加载阶段说明",
"type": "n8n-nodes-base.stickyNote",
"position": [
400,
240
],
"parameters": {
"width": 520,
"height": 520,
"content": "## 🔷 加载阶段"
},
"typeVersion": 1
},
{
"id": "bd19674e-a352-48fc-8c06-88ad296e000f",
"name": "便签",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1860,
60
],
"parameters": {
"color": 7,
"width": 2800,
"height": 720,
"content": "# CAD (BIM) 的 ETL 流程"
},
"typeVersion": 1
}
],
"active": false,
"pinData": {
"Start - Click to begin": [
{
"json": {}
}
]
},
"settings": {
"executionOrder": "v1"
},
"versionId": "f166c01d-5ae5-418e-af24-d11c81839f53",
"connections": {
"Start - Click to begin": {
"main": [
[
{
"node": "Setup - Define file paths",
"type": "main",
"index": 0
}
]
]
},
"Setup - Define file paths": {
"main": [
[
{
"node": "Extract - Run Revit converter",
"type": "main",
"index": 0
}
]
]
},
"Transform - Clean wall data": {
"main": [
[
{
"node": "Transform - Group by Type Name & sum Volume",
"type": "main",
"index": 0
}
]
]
},
"Extract - Parse Excel to data": {
"main": [
[
{
"node": "Transform - Filter only OST_Walls",
"type": "main",
"index": 0
}
]
]
},
"Extract - Run Revit converter": {
"main": [
[
{
"node": "Check - Did extraction succeed?",
"type": "main",
"index": 0
}
]
]
},
"Check - Did extraction succeed?": {
"main": [
[
{
"node": "Error - Show what went wrong",
"type": "main",
"index": 0
}
],
[
{
"node": "Success - Create Excel filename",
"type": "main",
"index": 0
}
]
]
},
"Success - Create Excel filename": {
"main": [
[
{
"node": "Extract - Read Excel file from disk",
"type": "main",
"index": 0
}
]
]
},
"Transform - Generate HTML Report": {
"main": [
[
{
"node": "Load - Save HTML Report",
"type": "main",
"index": 0
}
]
]
},
"Transform - Filter only OST_Walls": {
"main": [
[
{
"node": "Transform - Clean wall data",
"type": "main",
"index": 0
}
]
]
},
"Extract - Read Excel file from disk": {
"main": [
[
{
"node": "Extract - Parse Excel to data",
"type": "main",
"index": 0
}
]
]
},
"Transform - Group by Type Name & sum Volume": {
"main": [
[
{
"node": "Transform - Generate HTML Report",
"type": "main",
"index": 0
}
]
]
}
}
}常见问题
如何使用这个工作流?
复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。
这个工作流适合什么场景?
高级 - 工程
需要付费吗?
本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。
相关工作流推荐
n8n_8_Revit_IFC_DWG转换_提取阶段_解析XLSX
提取并解析Revit模型数据至结构化Excel格式
If
Set
Manual Trigger
+4
13 节点Artem Boiko
工程
我的工作流11
根据Excel标准验证CAD-BIM文件(Revit/IFC/DWG/DGN)
If
Set
Code
+6
26 节点Artem
工程
n8n_3_CAD-BIM-批量转换器-管道
批量转换 CAD/BIM 文件为 XLSX/DAE,带验证和报告
If
Set
Code
+14
82 节点Artem Boiko
文档提取
n8n_7_Revit和IFC碳足迹CO2估算器
使用AI分类计算Revit/IFC模型的隐含碳(CO2)
If
Set
Code
+13
55 节点Artem Boiko
AI 摘要总结
n8n_6_使用LLM为Revit和IFC进行施工造价估算
使用GPT-4和Claude基于Revit/IFC模型估算施工成本
If
Set
Code
+13
55 节点Artem Boiko
AI 摘要总结
n8n_1_Revit_IFC_DWG_转换_简化版
使用DataDrivenConstruction将CAD/BIM文件转换为Excel和3D模型
Set
Manual Trigger
Execute Command
+1
5 节点Artem Boiko
工程