1982 lines
94 KiB
HTML
1982 lines
94 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>知识图谱可视化</title>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/layui/2.8.12/css/layui.min.css">
|
|
<style>
|
|
body {
|
|
background: #f0f2f5;
|
|
font-family: 'Microsoft YaHei', sans-serif;
|
|
}
|
|
|
|
.kg-container {
|
|
height: 100vh;
|
|
display: flex;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.kg-sidebar {
|
|
width: 350px;
|
|
background: #fff;
|
|
border-right: 1px solid #e6e6e6;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
|
min-width: 200px;
|
|
max-width: 600px;
|
|
}
|
|
|
|
.kg-sidebar-header {
|
|
padding: 15px 20px;
|
|
background: linear-gradient(135deg, #1E9FFF, #5FB878);
|
|
color: white;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
}
|
|
|
|
.kg-resizer {
|
|
width: 5px;
|
|
background: #e6e6e6;
|
|
cursor: col-resize;
|
|
position: relative;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.kg-resizer:hover {
|
|
background: #1E9FFF;
|
|
}
|
|
|
|
.kg-main {
|
|
flex: 1;
|
|
padding: 15px;
|
|
overflow-y: auto;
|
|
background: #f0f2f5;
|
|
}
|
|
|
|
/* 自定义滚动条 */
|
|
.kg-sidebar::-webkit-scrollbar,
|
|
.kg-main::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.kg-sidebar::-webkit-scrollbar-track,
|
|
.kg-main::-webkit-scrollbar-track {
|
|
background: #f1f1f1;
|
|
}
|
|
|
|
.kg-sidebar::-webkit-scrollbar-thumb,
|
|
.kg-main::-webkit-scrollbar-thumb {
|
|
background: #c1c1c1;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.kg-sidebar::-webkit-scrollbar-thumb:hover,
|
|
.kg-main::-webkit-scrollbar-thumb:hover {
|
|
background: #a8a8a8;
|
|
}
|
|
|
|
/* 调整表格样式 */
|
|
.layui-table-cell {
|
|
height: auto;
|
|
line-height: 28px;
|
|
}
|
|
|
|
/* 节点头部样式 */
|
|
.node-header {
|
|
background: linear-gradient(135deg, #1E9FFF, #5FB878);
|
|
color: white;
|
|
padding: 20px;
|
|
border-radius: 6px;
|
|
margin-bottom: 15px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.node-header-left {
|
|
flex: 1;
|
|
}
|
|
|
|
.node-header-right {
|
|
margin-left: 20px;
|
|
}
|
|
|
|
/* 树节点选中状态 */
|
|
.layui-tree-entry.selected {
|
|
background-color: #e6f7ff !important;
|
|
border-left: 3px solid #1E9FFF;
|
|
}
|
|
|
|
.layui-tree-entry.selected .layui-tree-txt {
|
|
color: #1E9FFF !important;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* 工具条样式 */
|
|
.table-toolbar {
|
|
margin-bottom: 10px;
|
|
padding: 10px;
|
|
background: #fff;
|
|
border-radius: 4px;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.column-filter {
|
|
display: inline-block;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.column-filter .layui-form-checkbox {
|
|
margin-right: 5px;
|
|
}
|
|
|
|
.project-division-tree {
|
|
width: 100%;
|
|
overflow: auto;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.project-division-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
table-layout: fixed;
|
|
}
|
|
|
|
.project-division-table tr {
|
|
border-bottom: 1px solid #e6e6e6;
|
|
}
|
|
|
|
.project-division-table td {
|
|
padding: 8px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.tree-node {
|
|
width: 60%;
|
|
text-align: left;
|
|
}
|
|
|
|
.tree-price {
|
|
width: 20%;
|
|
text-align: right;
|
|
}
|
|
|
|
.tree-indent {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.tree-toggle {
|
|
cursor: pointer;
|
|
margin-right: 5px;
|
|
display: inline-block;
|
|
width: 16px;
|
|
height: 16px;
|
|
text-align: center;
|
|
line-height: 16px;
|
|
}
|
|
|
|
.tree-row:hover {
|
|
background-color: #f2f2f2;
|
|
}
|
|
|
|
.tree-children {
|
|
width: 100%;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="kg-container">
|
|
<!-- 左侧边栏 -->
|
|
<div class="kg-sidebar">
|
|
<div class="kg-sidebar-header">
|
|
<i class="layui-icon layui-icon-tree"></i>
|
|
知识图谱目录
|
|
</div>
|
|
<!-- 左侧树形菜单容器 -->
|
|
<div id="left-tree" style="padding: 10px; overflow-y: auto; flex: 1;"></div>
|
|
</div>
|
|
|
|
<!-- 拖拽分隔条 -->
|
|
<div class="kg-resizer" id="resizer"></div>
|
|
|
|
<!-- 右侧主内容 -->
|
|
<div class="kg-main">
|
|
<!-- 欢迎页 -->
|
|
<div id="welcome-container" class="layui-text" style="text-align: center; margin-top: 100px;">
|
|
<i class="layui-icon layui-icon-tree" style="font-size: 64px; color: #d2d2d2;"></i>
|
|
<h2>欢迎使用知识图谱可视化系统</h2>
|
|
<p>点击左侧树状结构中的任意节点,查看详细信息</p>
|
|
</div>
|
|
|
|
<!-- 节点详情容器 -->
|
|
<div id="node-details" style="display: none;">
|
|
<!-- 节点头部信息 -->
|
|
<div class="node-header">
|
|
<div class="node-header-left">
|
|
<h2 id="node-title"></h2>
|
|
<p id="node-id"></p>
|
|
</div>
|
|
<div class="node-header-right">
|
|
<button class="layui-btn layui-btn-normal" id="cost-preview-btn">
|
|
<i class="layui-icon layui-icon-rmb"></i> 费用预览
|
|
</button>
|
|
<button class="layui-btn layui-btn-normal" id="export-excel-btn" style="margin-left: 10px;">
|
|
<i class="layui-icon layui-icon-download-circle"></i> 导出工程Excel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 树状结构容器 -->
|
|
<div class="layui-card">
|
|
<div class="layui-card-body">
|
|
<div id="tree-structure-container"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/layui/2.8.12/layui.min.js"></script>
|
|
<script>
|
|
layui.use(['layer', 'element', 'table', 'treeTable', 'tree', 'util', 'form'], function() {
|
|
const layer = layui.layer;
|
|
const element = layui.element;
|
|
const table = layui.table;
|
|
const treeTable = layui.treeTable;
|
|
const tree = layui.tree;
|
|
const util = layui.util;
|
|
const form = layui.form;
|
|
|
|
class KnowledgeGraphUI {
|
|
constructor() {
|
|
this.selectedNodeId = null;
|
|
this.costPreviewData = new Map(); // 存储费用预览数据
|
|
this.selectedTreeNode = null; // 存储当前选中的树节点DOM
|
|
this.projectPropertyChildrenData = new Map(); // 存储工程属性节点的子节点数据
|
|
this.init();
|
|
this.initResizer();
|
|
this.initColumnConfig();
|
|
this.initToolbar();
|
|
}
|
|
|
|
// 初始化列配置
|
|
initColumnConfig() {
|
|
// 不同节点类型的列配置
|
|
this.columnConfigs = {
|
|
'default': {
|
|
children: [
|
|
{field: 'title', title: '节点类型', minWidth: 200, show: true},
|
|
{field: 'name', title: '名称', minWidth: 150, show: true},
|
|
{field: 'guid', title: 'GUID', minWidth: 280, show: true},
|
|
{field: 'type', title: '类型', minWidth: 100, show: true}
|
|
],
|
|
costPreview: [
|
|
{field: 'name', title: '费用项名称', minWidth: 200, show: true},
|
|
{field: 'amount', title: '数量', width: 100, show: true},
|
|
{field: 'unit', title: '单位', width: 80, show: true},
|
|
{field: 'unitPrice', title: '单价', width: 120, show: true},
|
|
{field: 'totalPrice', title: '合计', width: 120, show: true}
|
|
]
|
|
},
|
|
'ProjectDivision': {
|
|
children: [
|
|
{field: 'title', title: '节点类型', minWidth: 200, show: true},
|
|
{field: 'name', title: '项目名称', minWidth: 200, show: true},
|
|
{field: 'guid', title: 'GUID', minWidth: 280, show: false},
|
|
{field: 'code', title: '项目编码', minWidth: 120, show: true},
|
|
{field: 'description', title: '描述', minWidth: 300, show: true}
|
|
]
|
|
},
|
|
'ProjectDivisionSet': {
|
|
children: [
|
|
{field: 'name', title: '项目名称', minWidth: 300, show: true},
|
|
{field: 'totalPriceWithTax', title: '合价含税', width: 150, show: true},
|
|
{field: 'totalPriceWithoutTax', title: '合价不含税', width: 150, show: true}
|
|
]
|
|
},
|
|
'Equipment': {
|
|
children: [
|
|
{field: 'title', title: '设备类型', minWidth: 150, show: true},
|
|
{field: 'name', title: '设备名称', minWidth: 200, show: true},
|
|
{field: 'model', title: '型号', minWidth: 150, show: true},
|
|
{field: 'specification', title: '规格', minWidth: 200, show: true},
|
|
{field: 'manufacturer', title: '厂家', minWidth: 150, show: true}
|
|
]
|
|
},
|
|
'MaterialandmachineCostItem': {
|
|
costPreview: [
|
|
{field: 'name', title: '费用项名称', minWidth: 200, show: true},
|
|
{field: 'type', title: '类型', width: 100, show: true},
|
|
{field: 'supply', title: '供货方', width: 120, show: true},
|
|
{field: 'code', title: '编码', width: 120, show: true},
|
|
{field: 'unit', title: '单位', width: 80, show: true},
|
|
{field: 'amount', title: '数量', width: 100, show: true},
|
|
{field: 'budgetPrice', title: '预算价', width: 120, show: true},
|
|
{field: 'marketPrice', title: '市场价', width: 120, show: true},
|
|
{field: 'totalBudget', title: '预算合价', width: 120, show: true},
|
|
{field: 'totalMarket', title: '市场合价', width: 120, show: true},
|
|
{field: 'priceDiff', title: '价差', width: 120, show: true}
|
|
]
|
|
},
|
|
'CostItem': {
|
|
costPreview: [
|
|
{field: 'name', title: '费用项名称', minWidth: 200, show: true},
|
|
{field: 'cost', title: '费用', width: 120, show: true},
|
|
{field: 'unique_id', title: '唯一ID', width: 280, show: false},
|
|
{field: 'id', title: 'ID', width: 100, show: false}
|
|
]
|
|
}
|
|
};
|
|
}
|
|
|
|
// 初始化工具条事件
|
|
initToolbar() {
|
|
// 子节点列设置
|
|
document.getElementById('column-filter-btn').addEventListener('click', () => {
|
|
this.toggleColumnFilters('children');
|
|
});
|
|
|
|
// 费用预览列设置
|
|
document.getElementById('cost-column-filter-btn').addEventListener('click', () => {
|
|
this.toggleColumnFilters('costPreview');
|
|
});
|
|
|
|
// 刷新按钮
|
|
document.getElementById('refresh-btn').addEventListener('click', () => {
|
|
if (this.selectedNodeId) {
|
|
this.selectNode(this.selectedNodeId, this.selectedTreeNode);
|
|
}
|
|
});
|
|
|
|
document.getElementById('cost-refresh-btn').addEventListener('click', () => {
|
|
if (this.selectedNodeId && this.costPreviewData.has(this.selectedNodeId)) {
|
|
this.renderCostPreviewData(this.costPreviewData.get(this.selectedNodeId));
|
|
}
|
|
});
|
|
|
|
// 费用预览按钮
|
|
document.getElementById('cost-item-btn').addEventListener('click', () => {
|
|
if (this.selectedNodeId && this.costPreviewData.has(this.selectedNodeId)) {
|
|
// 更新按钮状态
|
|
document.getElementById('cost-item-btn').classList.add('layui-btn-disabled');
|
|
document.getElementById('material-machine-btn').classList.remove('layui-btn-disabled');
|
|
|
|
// 渲染费用项数据
|
|
this.renderCostPreviewTable(this.costPreviewData.get(this.selectedNodeId), 'CostItem');
|
|
}
|
|
});
|
|
|
|
// 材机列表按钮
|
|
document.getElementById('material-machine-btn').addEventListener('click', () => {
|
|
if (this.selectedNodeId && this.costPreviewData.has(this.selectedNodeId)) {
|
|
// 更新按钮状态
|
|
document.getElementById('material-machine-btn').classList.add('layui-btn-disabled');
|
|
document.getElementById('cost-item-btn').classList.remove('layui-btn-disabled');
|
|
|
|
// 渲染材机列表数据
|
|
this.renderCostPreviewTable(this.costPreviewData.get(this.selectedNodeId), 'MaterialandmachineCostItem');
|
|
}
|
|
});
|
|
}
|
|
|
|
// 切换列过滤器显示
|
|
toggleColumnFilters(type) {
|
|
const filterId = type === 'children' ? 'column-filters' : 'cost-column-filters';
|
|
const filterDiv = document.getElementById(filterId);
|
|
|
|
if (filterDiv.style.display === 'none') {
|
|
this.renderColumnFilters(type);
|
|
filterDiv.style.display = 'block';
|
|
} else {
|
|
filterDiv.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// 渲染列过滤器
|
|
renderColumnFilters(type) {
|
|
const nodeType = this.getCurrentNodeType();
|
|
const config = this.columnConfigs[nodeType] || this.columnConfigs['default'];
|
|
const columns = config[type] || [];
|
|
|
|
const filterId = type === 'children' ? 'column-filters' : 'cost-column-filters';
|
|
const filterDiv = document.getElementById(filterId);
|
|
|
|
let html = '<div class="layui-form">';
|
|
columns.forEach((col, index) => {
|
|
const checked = col.show ? 'checked' : '';
|
|
html += `
|
|
<div class="column-filter">
|
|
<input type="checkbox" name="col_${type}_${index}" title="${col.title}" ${checked} lay-filter="column-filter">
|
|
</div>
|
|
`;
|
|
});
|
|
html += '</div>';
|
|
|
|
filterDiv.innerHTML = html;
|
|
form.render('checkbox');
|
|
|
|
// 监听复选框变化
|
|
form.on('checkbox(column-filter)', (data) => {
|
|
const match = data.elem.name.match(/col_(\w+)_(\d+)/);
|
|
if (match) {
|
|
const tableType = match[1];
|
|
const colIndex = parseInt(match[2]);
|
|
const isChecked = data.elem.checked;
|
|
|
|
this.updateColumnVisibility(tableType, colIndex, isChecked);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 更新列可见性
|
|
updateColumnVisibility(type, colIndex, isVisible) {
|
|
const nodeType = this.getCurrentNodeType();
|
|
const config = this.columnConfigs[nodeType] || this.columnConfigs['default'];
|
|
|
|
if (config[type] && config[type][colIndex]) {
|
|
config[type][colIndex].show = isVisible;
|
|
|
|
// 重新渲染表格
|
|
if (type === 'children') {
|
|
this.renderChildrenTreeTable(this.currentChildrenData || []);
|
|
} else if (type === 'costPreview') {
|
|
// 使用当前选中的节点ID获取费用数据
|
|
const costData = this.costPreviewData.get(this.selectedNodeId);
|
|
if (costData) {
|
|
// 检查当前显示的是哪种类型的费用数据
|
|
const costItemBtn = document.getElementById('cost-item-btn');
|
|
const materialMachineBtn = document.getElementById('material-machine-btn');
|
|
|
|
if (costItemBtn.classList.contains('layui-btn-disabled')) {
|
|
this.renderCostPreviewTable(costData, 'CostItem');
|
|
} else if (materialMachineBtn.classList.contains('layui-btn-disabled')) {
|
|
this.renderCostPreviewTable(costData, 'MaterialandmachineCostItem');
|
|
} else {
|
|
this.renderCostPreviewTable(costData);
|
|
}
|
|
} else {
|
|
this.renderCostPreviewTable(null);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 获取当前节点类型
|
|
getCurrentNodeType() {
|
|
// 检查当前选中节点的类型
|
|
if (this.selectedNodeId) {
|
|
const node = this.allNodes ? this.allNodes[this.selectedNodeId] : null;
|
|
if (node) {
|
|
// 根据节点类型返回对应的配置
|
|
if (node.type === 'ProjectDivisionSet' ||
|
|
(node.name && (node.name.includes('项目划分') || node.name.includes('工程量')))
|
|
) {
|
|
return 'ProjectDivisionSet';
|
|
} else if (node.type === 'ProjectPropertySet' ||
|
|
(node.name && node.name === '工程属性')
|
|
) {
|
|
return 'ProjectPropertySet';
|
|
} else if (node.type === 'Equipment') {
|
|
return 'Equipment';
|
|
} else if (node.type === 'ProjectDivision') {
|
|
return 'ProjectDivision';
|
|
}
|
|
}
|
|
}
|
|
// 默认返回default配置
|
|
return 'default';
|
|
}
|
|
|
|
async init() {
|
|
await this.loadTree();
|
|
}
|
|
|
|
// 初始化拖拽功能
|
|
initResizer() {
|
|
const resizer = document.getElementById('resizer');
|
|
const sidebar = document.querySelector('.kg-sidebar');
|
|
let isResizing = false;
|
|
|
|
resizer.addEventListener('mousedown', (e) => {
|
|
isResizing = true;
|
|
document.addEventListener('mousemove', handleMouseMove);
|
|
document.addEventListener('mouseup', handleMouseUp);
|
|
e.preventDefault();
|
|
});
|
|
|
|
function handleMouseMove(e) {
|
|
if (!isResizing) return;
|
|
const newWidth = e.clientX;
|
|
if (newWidth >= 200 && newWidth <= 600) {
|
|
sidebar.style.width = newWidth + 'px';
|
|
}
|
|
}
|
|
|
|
function handleMouseUp() {
|
|
isResizing = false;
|
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
}
|
|
}
|
|
|
|
async loadTree() {
|
|
try {
|
|
// 显示加载层
|
|
const loadIndex = layer.load(2);
|
|
|
|
const response = await fetch('/api/tree');
|
|
const data = await response.json();
|
|
|
|
// 关闭加载层
|
|
layer.close(loadIndex);
|
|
|
|
if (data.error) {
|
|
layer.msg('加载失败: ' + data.error, {icon: 2});
|
|
return;
|
|
}
|
|
|
|
// 存储所有节点数据,用于父节点查找
|
|
this.allNodes = data.all_nodes;
|
|
|
|
// 预处理费用预览数据
|
|
this.processCostPreviewData(data.roots);
|
|
|
|
this.renderLayuiTree(data.roots);
|
|
} catch (error) {
|
|
layer.msg('网络错误: ' + error.message, {icon: 2});
|
|
}
|
|
}
|
|
|
|
// 预处理费用预览数据
|
|
processCostPreviewData(nodes) {
|
|
nodes.forEach(node => {
|
|
if (node.children) {
|
|
// 查找费用预览节点
|
|
const costPreviewNode = node.children.find(child =>
|
|
child.name && child.name.includes('费用预览')
|
|
);
|
|
|
|
if (costPreviewNode && costPreviewNode.children) {
|
|
// 将费用预览数据存储到当前节点
|
|
this.costPreviewData.set(node.id, costPreviewNode.children);
|
|
}
|
|
|
|
// 递归处理子节点
|
|
this.processCostPreviewData(node.children);
|
|
}
|
|
});
|
|
}
|
|
|
|
renderLayuiTree(nodes) {
|
|
// 将API返回的数据转换为Layui tree组件所需的格式
|
|
const treeData = this.formatTreeData(nodes, 1);
|
|
|
|
// 渲染树组件
|
|
tree.render({
|
|
elem: '#left-tree',
|
|
data: treeData,
|
|
showLine: true,
|
|
click: (obj) => {
|
|
this.selectNode(obj.data.id, obj.elem);
|
|
}
|
|
});
|
|
}
|
|
|
|
formatTreeData(nodes, level = 1) {
|
|
return nodes.map(node => {
|
|
// 检查是否是费用预览节点
|
|
if (node.name && node.name.includes('费用预览')) {
|
|
// 将费用预览数据存储到父节点
|
|
if (node.children) {
|
|
// 找到父节点ID(这里需要根据实际数据结构调整)
|
|
const parentId = this.findParentNodeId(node.id);
|
|
if (parentId) {
|
|
this.costPreviewData.set(parentId, node.children);
|
|
}
|
|
}
|
|
return null; // 不显示费用预览节点
|
|
}
|
|
|
|
const item = {
|
|
title: node.name || node.label, // 只显示名称
|
|
id: node.id,
|
|
spread: level <= 3 // 自动展开到第三层级
|
|
};
|
|
|
|
// 检查是否是工程属性节点
|
|
const isProjectPropertyNode = node.type === 'ProjectPropertySet' || (node.name && node.name === '工程属性');
|
|
// 检查是否是项目划分集节点
|
|
const isProjectDivisionSet = node.type === 'ProjectDivisionSet' || (node.name && (node.name.includes('项目划分') || node.name.includes('工程量')));
|
|
|
|
if (node.children && node.children.length > 0) {
|
|
// 如果是工程属性节点,不在树中显示其子节点,而是存储起来
|
|
if (isProjectPropertyNode) {
|
|
// 存储工程属性节点的子节点,以便在右侧显示
|
|
this.projectPropertyChildrenData.set(node.id, node.children);
|
|
|
|
// 检查是否有费用预览子节点
|
|
const costPreviewChild = node.children.find(child =>
|
|
child.name && child.name.includes('费用预览')
|
|
);
|
|
if (costPreviewChild && costPreviewChild.children) {
|
|
this.costPreviewData.set(node.id, costPreviewChild.children);
|
|
}
|
|
} else if (isProjectDivisionSet) {
|
|
// 对于项目划分集节点,正常处理子节点
|
|
const filteredChildren = this.formatTreeData(node.children, level + 1).filter(child => child !== null);
|
|
if (filteredChildren.length > 0) {
|
|
item.children = filteredChildren;
|
|
}
|
|
|
|
// 检查是否有费用预览子节点
|
|
const costPreviewChild = node.children.find(child =>
|
|
child.name && child.name.includes('费用预览')
|
|
);
|
|
if (costPreviewChild && costPreviewChild.children) {
|
|
this.costPreviewData.set(node.id, costPreviewChild.children);
|
|
}
|
|
} else {
|
|
// 对于非工程属性节点,正常处理子节点
|
|
const filteredChildren = this.formatTreeData(node.children, level + 1).filter(child => child !== null);
|
|
if (filteredChildren.length > 0) {
|
|
item.children = filteredChildren;
|
|
}
|
|
|
|
// 检查是否有费用预览子节点
|
|
const costPreviewChild = node.children.find(child =>
|
|
child.name && child.name.includes('费用预览')
|
|
);
|
|
if (costPreviewChild && costPreviewChild.children) {
|
|
this.costPreviewData.set(node.id, costPreviewChild.children);
|
|
}
|
|
}
|
|
}
|
|
|
|
return item;
|
|
}).filter(item => item !== null);
|
|
}
|
|
|
|
// 查找父节点ID的辅助方法
|
|
findParentNodeId(nodeId) {
|
|
// 遍历所有节点,查找包含该节点作为子节点的父节点
|
|
for (const [parentId, node] of Object.entries(this.allNodes || {})) {
|
|
if (node.children && node.children.some(child => child.id === nodeId)) {
|
|
return parseInt(parentId);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async selectNode(nodeId, elem) {
|
|
this.selectedNodeId = nodeId;
|
|
this.selectedTreeNode = elem;
|
|
|
|
// 更新选中状态
|
|
this.updateSelectedState(elem);
|
|
|
|
// 显示加载层
|
|
const loadIndex = layer.load(2);
|
|
|
|
try {
|
|
// 获取节点基本信息
|
|
const response = await fetch(`/api/node/${nodeId}`);
|
|
const data = await response.json();
|
|
|
|
if (data.error) {
|
|
layer.msg('加载失败: ' + data.error, {icon: 2});
|
|
layer.close(loadIndex);
|
|
return;
|
|
}
|
|
|
|
// 获取节点费用信息
|
|
const costResponse = await fetch(`/api/node/${nodeId}/cost`);
|
|
const costData = await costResponse.json();
|
|
|
|
// 存储费用数据
|
|
if (!costData.error) {
|
|
this.costPreviewData.set(nodeId, costData);
|
|
}
|
|
|
|
// 检查是否是工程属性节点,如果是则使用已存储的子节点数据
|
|
const isProjectPropertyNode =
|
|
data.node && data.node.labels &&
|
|
data.node.labels.includes('ProjectPropertySet');
|
|
|
|
if (isProjectPropertyNode && this.projectPropertyChildrenData.has(nodeId)) {
|
|
// 使用预先存储的工程属性子节点数据
|
|
const storedChildren = this.projectPropertyChildrenData.get(nodeId);
|
|
// 将存储的子节点数据转换为API返回的格式
|
|
const formattedChildren = storedChildren.map(child => {
|
|
return {
|
|
id: child.id,
|
|
labels: [child.type],
|
|
properties: {
|
|
name: child.name,
|
|
...child
|
|
}
|
|
};
|
|
});
|
|
|
|
// 合并API返回的子节点和存储的子节点
|
|
const allChildren = [...(data.children || []), ...formattedChildren];
|
|
|
|
// 递归获取所有子节点
|
|
const fullChildren = await this.getAllChildrenRecursively(allChildren);
|
|
|
|
// 关闭加载层
|
|
layer.close(loadIndex);
|
|
|
|
this.renderNodeDetails(data.node, fullChildren);
|
|
} else {
|
|
// 对于非工程属性节点,递归获取所有子节点
|
|
const fullChildren = await this.getAllChildrenRecursively(data.children || []);
|
|
|
|
// 关闭加载层
|
|
layer.close(loadIndex);
|
|
|
|
this.renderNodeDetails(data.node, fullChildren);
|
|
}
|
|
} catch (error) {
|
|
layer.close(loadIndex);
|
|
layer.msg('网络错误: ' + error.message, {icon: 2});
|
|
}
|
|
}
|
|
|
|
updateSelectedState(elem) {
|
|
// 移除所有选中状态
|
|
document.querySelectorAll('.layui-tree-entry.selected').forEach(entry => {
|
|
entry.classList.remove('selected');
|
|
});
|
|
|
|
// 添加当前选中状态
|
|
if (elem && elem[0]) {
|
|
elem[0].classList.add('selected');
|
|
}
|
|
}
|
|
|
|
renderNodeDetails(node, children) {
|
|
// 隐藏欢迎页,显示节点详情
|
|
document.getElementById('welcome-container').style.display = 'none';
|
|
document.getElementById('node-details').style.display = 'block';
|
|
|
|
// 设置节点标题和ID
|
|
document.getElementById('node-title').innerText = node.labels ? node.labels.join(', ') : '节点';
|
|
document.getElementById('node-id').innerHTML = `<i class="layui-icon layui-icon-key"></i> 节点ID: ${node.id}`;
|
|
|
|
// 存储当前子节点数据
|
|
this.currentChildrenData = children || [];
|
|
|
|
// 渲染树状结构
|
|
this.renderTreeStructure(node, this.currentChildrenData);
|
|
|
|
// 添加费用预览按钮点击事件
|
|
document.getElementById('cost-preview-btn').addEventListener('click', () => {
|
|
this.showCostPreviewDialog(node.id);
|
|
});
|
|
|
|
// 添加导出Excel按钮点击事件
|
|
document.getElementById('export-excel-btn').addEventListener('click', () => {
|
|
this.exportToExcel(node.id);
|
|
});
|
|
}
|
|
|
|
renderTreeStructure(node, children) {
|
|
// 获取表格容器
|
|
const container = document.getElementById('tree-structure-container');
|
|
|
|
// 清空容器
|
|
container.innerHTML = '';
|
|
|
|
// 检查是否是项目划分集节点或其子节点
|
|
const nodeType = this.getCurrentNodeType();
|
|
let isProjectDivisionSet = nodeType === 'ProjectDivisionSet';
|
|
|
|
// 检查当前节点是否为项目划分集的子节点
|
|
if (!isProjectDivisionSet && this.selectedNodeId) {
|
|
// 尝试查找父节点
|
|
const parentId = this.findParentNodeId(this.selectedNodeId);
|
|
if (parentId) {
|
|
const parentNode = this.allNodes[parentId];
|
|
if (parentNode && (parentNode.type === 'ProjectDivisionSet' ||
|
|
(parentNode.name && (parentNode.name.includes('项目划分') || parentNode.name.includes('工程量'))))) {
|
|
isProjectDivisionSet = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 准备树表格数据
|
|
const treeData = this.buildTreeStructureData(node, children);
|
|
|
|
// 创建树状表格容器
|
|
const treeContainer = document.createElement('div');
|
|
treeContainer.className = 'project-division-tree';
|
|
|
|
// 创建表格
|
|
const table = document.createElement('table');
|
|
table.className = 'project-division-table';
|
|
|
|
// 添加表格内容
|
|
const tableBody = document.createElement('tbody');
|
|
tableBody.innerHTML = this.buildTreeStructureHTML(treeData);
|
|
table.appendChild(tableBody);
|
|
|
|
// 添加到容器
|
|
treeContainer.appendChild(table);
|
|
container.appendChild(treeContainer);
|
|
|
|
// 添加样式
|
|
this.addTreeStructureStyles();
|
|
|
|
// 添加展开/折叠事件监听
|
|
this.addTreeToggleListeners();
|
|
}
|
|
|
|
showCostPreviewDialog(nodeId) {
|
|
// 获取费用预览数据
|
|
const costData = this.costPreviewData.get(parseInt(nodeId));
|
|
|
|
if (!costData) {
|
|
layer.msg('该节点没有费用预览数据');
|
|
return;
|
|
}
|
|
|
|
// 创建弹窗
|
|
layer.open({
|
|
type: 1,
|
|
title: '费用预览',
|
|
area: ['800px', '600px'],
|
|
content: '<div class="layui-tab layui-tab-card">' +
|
|
'<ul class="layui-tab-title">' +
|
|
'<li class="layui-this">费用项</li>' +
|
|
'<li>材机列表</li>' +
|
|
'</ul>' +
|
|
'<div class="layui-tab-content">' +
|
|
'<div class="layui-tab-item layui-show">' +
|
|
'<div id="cost-preview-container"></div>' +
|
|
'</div>' +
|
|
'<div class="layui-tab-item">' +
|
|
'<div id="material-machine-container"></div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>',
|
|
success: () => {
|
|
// 渲染费用预览表格
|
|
this.renderCostPreviewTable(costData, 'CostItem', 'cost-preview-container');
|
|
this.renderCostPreviewTable(costData, 'MaterialandmachineCostItem', 'material-machine-container');
|
|
|
|
// 初始化Tab
|
|
element.render('tab');
|
|
}
|
|
});
|
|
}
|
|
|
|
renderPropertyTable(node) {
|
|
const propertyData = [];
|
|
|
|
if (node.properties && Object.keys(node.properties).length > 0) {
|
|
Object.entries(node.properties).forEach(([key, value]) => {
|
|
propertyData.push({
|
|
key: key,
|
|
value: this.formatValue(value)
|
|
});
|
|
});
|
|
}
|
|
|
|
// 使用Layui表格组件渲染属性
|
|
table.render({
|
|
elem: '#property-table-container',
|
|
data: propertyData,
|
|
cols: [[
|
|
{field: 'key', title: '属性名', width: '30%', sort: true},
|
|
{field: 'value', title: '属性值', width: '70%'}
|
|
]],
|
|
skin: 'line',
|
|
even: true,
|
|
text: {
|
|
none: '该节点没有属性'
|
|
}
|
|
});
|
|
}
|
|
|
|
renderChildrenTreeTable(children) {
|
|
// 获取当前节点类型的列配置
|
|
const nodeType = this.getCurrentNodeType();
|
|
|
|
// 检查是否是项目划分集节点或其子节点
|
|
let isProjectDivisionSet = nodeType === 'ProjectDivisionSet';
|
|
|
|
// 检查当前节点是否为项目划分集的子节点
|
|
if (!isProjectDivisionSet && this.selectedNodeId) {
|
|
// 尝试查找父节点
|
|
const parentId = this.findParentNodeId(this.selectedNodeId);
|
|
if (parentId) {
|
|
const parentNode = this.allNodes[parentId];
|
|
if (parentNode && (parentNode.type === 'ProjectDivisionSet' ||
|
|
(parentNode.name && (parentNode.name.includes('项目划分') || parentNode.name.includes('工程量'))))) {
|
|
isProjectDivisionSet = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 获取适用的列配置
|
|
const config = isProjectDivisionSet ?
|
|
this.columnConfigs['ProjectDivisionSet'] :
|
|
(this.columnConfigs[nodeType] || this.columnConfigs['default']);
|
|
|
|
const columnConfig = config.children || this.columnConfigs['default'].children;
|
|
|
|
// 获取表格容器
|
|
const container = document.getElementById('children-table-container');
|
|
|
|
// 清空容器
|
|
container.innerHTML = '';
|
|
|
|
// 如果是项目划分集节点或其子节点,使用自定义树状表格渲染
|
|
if (isProjectDivisionSet) {
|
|
// 准备树表格数据
|
|
const treeData = this.buildProjectDivisionTreeData(children);
|
|
|
|
// 使用自定义方法渲染项目划分集树状表格
|
|
this.renderProjectDivisionAsTree(treeData);
|
|
} else {
|
|
// 对于非项目划分集节点,使用原有的渲染方式
|
|
// 准备树表格数据
|
|
const treeData = [];
|
|
|
|
// 处理子节点数据
|
|
for (const child of children) {
|
|
const item = {
|
|
id: child.id,
|
|
name: child.properties ? child.properties.name : '',
|
|
title: child.labels ? child.labels.join(', ') : '子节点'
|
|
};
|
|
|
|
// 添加属性
|
|
if (child.properties) {
|
|
// 根据配置添加属性
|
|
columnConfig.forEach(col => {
|
|
if (child.properties[col.field]) {
|
|
item[col.field] = child.properties[col.field];
|
|
}
|
|
});
|
|
|
|
// 添加常用属性
|
|
if (child.properties.name) item.name = child.properties.name;
|
|
if (child.properties.GUID) item.guid = child.properties.GUID;
|
|
if (child.properties.type) item.type = child.properties.type;
|
|
if (child.properties.code) item.code = child.properties.code;
|
|
if (child.properties.description) item.description = child.properties.description;
|
|
|
|
// 将所有属性序列化为JSON存储,用于详情查看
|
|
item.allProperties = JSON.stringify(child.properties);
|
|
}
|
|
|
|
treeData.push(item);
|
|
}
|
|
|
|
// 构建列配置
|
|
const cols = [];
|
|
|
|
// 添加可见的列
|
|
columnConfig.forEach(col => {
|
|
if (col.show) {
|
|
cols.push({
|
|
field: col.field,
|
|
title: col.title,
|
|
minWidth: col.minWidth || col.width || 150,
|
|
width: col.width
|
|
});
|
|
}
|
|
});
|
|
|
|
// 添加操作列
|
|
cols.push({
|
|
title: '操作',
|
|
width: 150,
|
|
templet: d => {
|
|
return `<div class="layui-btn-group">
|
|
<button class="layui-btn layui-btn-xs" lay-event="view">查看</button>
|
|
<button class="layui-btn layui-btn-xs layui-btn-normal" lay-event="detail">详情</button>
|
|
</div>`;
|
|
}
|
|
});
|
|
|
|
// 使用 treeTable 渲染
|
|
treeTable.render({
|
|
elem: '#children-table-container',
|
|
data: treeData,
|
|
tree: {
|
|
iconIndex: 0,
|
|
isPidData: false,
|
|
idName: 'id',
|
|
childName: 'children',
|
|
view: {
|
|
showIcon: true,
|
|
showLine: true,
|
|
indent: 20,
|
|
dblClickExpand: false
|
|
}
|
|
},
|
|
cols: [cols],
|
|
text: {
|
|
none: '该节点没有子节点'
|
|
},
|
|
page: false
|
|
});
|
|
|
|
// 监听表格操作
|
|
treeTable.on('tool(children-table-container)', obj => {
|
|
const data = obj.data;
|
|
if (obj.event === 'view') {
|
|
this.selectNode(data.id);
|
|
} else if (obj.event === 'detail') {
|
|
this.showNodeDetailLayer(data);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
renderProjectDivisionAsTree(treeData) {
|
|
const container = document.getElementById('children-table-container');
|
|
|
|
// 创建树状表格容器
|
|
const treeContainer = document.createElement('div');
|
|
treeContainer.className = 'project-division-tree';
|
|
|
|
// 创建表格
|
|
const table = document.createElement('table');
|
|
table.className = 'project-division-table';
|
|
|
|
// 添加表格内容
|
|
const tableBody = document.createElement('tbody');
|
|
tableBody.innerHTML = this.buildProjectDivisionTreeHTML(treeData);
|
|
table.appendChild(tableBody);
|
|
|
|
// 添加到容器
|
|
treeContainer.appendChild(table);
|
|
container.appendChild(treeContainer);
|
|
|
|
// 添加样式
|
|
this.addProjectDivisionTreeStyles();
|
|
|
|
// 添加展开/折叠事件监听
|
|
this.addTreeToggleListeners();
|
|
}
|
|
|
|
buildProjectDivisionTreeData(children) {
|
|
const treeData = [];
|
|
|
|
// 处理子节点数据
|
|
for (const child of children) {
|
|
const item = {
|
|
id: child.id,
|
|
children: []
|
|
};
|
|
|
|
// 添加属性
|
|
if (child.properties) {
|
|
// 添加标准属性
|
|
item.mark = child.properties.mark || '';
|
|
item.code = child.properties.code || '';
|
|
item.name = child.properties.name || '';
|
|
item.specification = child.properties.specification || child.properties.规格型号 || '';
|
|
item.unit = child.properties.unit || child.properties.单位 || '';
|
|
item.formula = child.properties.formula || child.properties.计算式 || '';
|
|
item.amount = child.properties.amount || child.properties.数量 || '';
|
|
item.unitPriceWithTax = child.properties.unitPriceWithTax || child.properties.单价含税 || '';
|
|
item.unitPriceWithoutTax = child.properties.unitPriceWithoutTax || child.properties.单价不含税 || '';
|
|
item.totalPriceWithTax = child.properties.totalPriceWithTax || child.properties.合价含税 || '';
|
|
item.totalPriceWithoutTax = child.properties.totalPriceWithoutTax || child.properties.合价不含税 || '';
|
|
item.taxIndex = child.properties.taxIndex || child.properties.取费索引 || '';
|
|
item.remark = child.properties.remark || child.properties.备注 || '';
|
|
|
|
// 将所有属性序列化为JSON存储,用于详情查看
|
|
item.allProperties = JSON.stringify(child.properties);
|
|
}
|
|
|
|
// 处理子节点
|
|
if (child.children && child.children.length > 0) {
|
|
item.children = this.buildProjectDivisionTreeData(child.children);
|
|
}
|
|
|
|
treeData.push(item);
|
|
}
|
|
|
|
return treeData;
|
|
}
|
|
|
|
buildProjectDivisionTreeHTML(treeData, level = 0) {
|
|
let html = '';
|
|
|
|
// 添加表头行
|
|
if (level === 0) {
|
|
html += `<tr class="tree-header">
|
|
<th class="tree-col-name">项目名称</th>
|
|
<th class="tree-col-spec">规格型号</th>
|
|
<th class="tree-col-unit">单位</th>
|
|
<th class="tree-col-formula">计算式</th>
|
|
<th class="tree-col-amount">数量</th>
|
|
<th class="tree-col-price">单价含税</th>
|
|
<th class="tree-col-price">单价不含税</th>
|
|
<th class="tree-col-total">合价含税</th>
|
|
<th class="tree-col-total">合价不含税</th>
|
|
<th class="tree-col-tax">取费表</th>
|
|
<th class="tree-col-remark">备注</th>
|
|
</tr>`;
|
|
}
|
|
|
|
// 生成行数据
|
|
treeData.forEach((item, index) => {
|
|
const hasChildren = item.children && item.children.length > 0;
|
|
const toggleClass = hasChildren ? 'tree-toggle' : '';
|
|
const toggleIcon = hasChildren ? '<i class="layui-icon layui-icon-triangle-d"></i>' : '<i class="layui-icon layui-icon-file"></i>';
|
|
const nodeClass = hasChildren ? 'parent-node' : 'leaf-node';
|
|
|
|
// 创建行
|
|
html += `<tr class="tree-row ${nodeClass}" data-id="${item.id}" data-level="${level}">`;
|
|
|
|
// 项目名称列(带缩进和图标)
|
|
html += `<td class="tree-col-name">
|
|
<div class="tree-indent" style="padding-left: ${level * 20}px">
|
|
<span class="${toggleClass}">${toggleIcon}</span>
|
|
<span class="node-code">${item.code ? item.code : ''}</span>
|
|
<span class="node-name">${item.name || '未命名项目'}</span>
|
|
</div>
|
|
</td>`;
|
|
|
|
// 规格型号列
|
|
html += `<td class="tree-col-spec">${item.specification || ''}</td>`;
|
|
|
|
// 单位列
|
|
html += `<td class="tree-col-unit">${item.unit || ''}</td>`;
|
|
|
|
// 计算式列
|
|
html += `<td class="tree-col-formula">${item.formula || ''}</td>`;
|
|
|
|
// 数量列
|
|
html += `<td class="tree-col-amount">${item.amount || ''}</td>`;
|
|
|
|
// 单价含税列
|
|
html += `<td class="tree-col-price">${item.unitPriceWithTax || ''}</td>`;
|
|
|
|
// 单价不含税列
|
|
html += `<td class="tree-col-price">${item.unitPriceWithoutTax || ''}</td>`;
|
|
|
|
// 合价含税列
|
|
html += `<td class="tree-col-total">${item.totalPriceWithTax || ''}</td>`;
|
|
|
|
// 合价不含税列
|
|
html += `<td class="tree-col-total">${item.totalPriceWithoutTax || ''}</td>`;
|
|
|
|
// 取费表列
|
|
html += `<td class="tree-col-tax">${item.taxIndex || ''}</td>`;
|
|
|
|
// 备注列
|
|
html += `<td class="tree-col-remark">${item.remark || ''}</td>`;
|
|
|
|
html += '</tr>';
|
|
|
|
// 递归处理子节点
|
|
if (hasChildren) {
|
|
html += this.buildProjectDivisionTreeHTML(item.children, level + 1);
|
|
}
|
|
});
|
|
|
|
return html;
|
|
}
|
|
|
|
addTreeToggleListeners() {
|
|
// 为所有可展开/折叠的节点添加点击事件
|
|
document.querySelectorAll('.tree-toggle').forEach(toggle => {
|
|
toggle.addEventListener('click', (e) => {
|
|
const icon = e.currentTarget.querySelector('i');
|
|
const row = e.currentTarget.closest('tr');
|
|
const level = parseInt(row.dataset.level);
|
|
const nodeId = row.dataset.id;
|
|
|
|
// 找到所有下一级节点
|
|
let nextRow = row.nextElementSibling;
|
|
let childrenVisible = icon.classList.contains('layui-icon-triangle-d');
|
|
|
|
while (nextRow && parseInt(nextRow.dataset.level) > level) {
|
|
if (parseInt(nextRow.dataset.level) === level + 1) {
|
|
// 直接子节点的显示/隐藏状态切换
|
|
if (childrenVisible) {
|
|
nextRow.style.display = 'none';
|
|
|
|
// 同时隐藏所有子节点的子节点
|
|
let childRow = nextRow.nextElementSibling;
|
|
while (childRow && parseInt(childRow.dataset.level) > level + 1) {
|
|
childRow.style.display = 'none';
|
|
childRow = childRow.nextElementSibling;
|
|
}
|
|
} else {
|
|
nextRow.style.display = '';
|
|
}
|
|
}
|
|
nextRow = nextRow.nextElementSibling;
|
|
}
|
|
|
|
// 切换图标
|
|
if (childrenVisible) {
|
|
icon.classList.remove('layui-icon-triangle-d');
|
|
icon.classList.add('layui-icon-triangle-r');
|
|
} else {
|
|
icon.classList.remove('layui-icon-triangle-r');
|
|
icon.classList.add('layui-icon-triangle-d');
|
|
}
|
|
|
|
e.stopPropagation();
|
|
});
|
|
});
|
|
|
|
// 为行添加点击事件,显示详细信息
|
|
document.querySelectorAll('.tree-row').forEach(row => {
|
|
row.addEventListener('click', (e) => {
|
|
if (!e.target.closest('.tree-toggle')) {
|
|
const nodeId = row.dataset.id;
|
|
if (nodeId) {
|
|
this.selectNode(nodeId);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
addProjectDivisionTreeStyles() {
|
|
// 检查是否已经添加了样式
|
|
if (!document.getElementById('project-division-tree-styles')) {
|
|
const style = document.createElement('style');
|
|
style.id = 'project-division-tree-styles';
|
|
style.textContent = `
|
|
.project-division-tree {
|
|
width: 100%;
|
|
overflow: auto;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.project-division-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
table-layout: fixed;
|
|
}
|
|
|
|
.project-division-table tr {
|
|
border-bottom: 1px solid #e6e6e6;
|
|
height: 40px;
|
|
}
|
|
|
|
.project-division-table td {
|
|
padding: 8px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.tree-node {
|
|
width: 70%;
|
|
text-align: left;
|
|
}
|
|
|
|
.tree-price {
|
|
width: 30%;
|
|
text-align: right;
|
|
}
|
|
|
|
.price-item {
|
|
color: #666;
|
|
font-size: 13px;
|
|
margin-bottom: 3px;
|
|
}
|
|
|
|
.tree-indent {
|
|
display: flex;
|
|
align-items: center;
|
|
position: relative;
|
|
}
|
|
|
|
.tree-toggle {
|
|
cursor: pointer;
|
|
margin-right: 8px;
|
|
display: inline-block;
|
|
width: 16px;
|
|
height: 16px;
|
|
text-align: center;
|
|
line-height: 16px;
|
|
}
|
|
|
|
.node-code {
|
|
color: #999;
|
|
font-size: 12px;
|
|
margin-right: 5px;
|
|
}
|
|
|
|
.node-name {
|
|
font-weight: normal;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.parent-node .node-name {
|
|
font-weight: bold;
|
|
color: #1E9FFF;
|
|
}
|
|
|
|
.tree-row:hover {
|
|
background-color: #f2f2f2;
|
|
}
|
|
|
|
/* 树状连接线样式 */
|
|
.tree-row .tree-indent {
|
|
position: relative;
|
|
}
|
|
|
|
.tree-row[data-level="1"] .tree-indent:before,
|
|
.tree-row[data-level="2"] .tree-indent:before,
|
|
.tree-row[data-level="3"] .tree-indent:before,
|
|
.tree-row[data-level="4"] .tree-indent:before,
|
|
.tree-row[data-level="5"] .tree-indent:before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 7px;
|
|
top: -8px;
|
|
height: calc(100% + 16px);
|
|
border-left: 1px dotted #ccc;
|
|
}
|
|
|
|
.tree-row[data-level="1"] .tree-indent:after,
|
|
.tree-row[data-level="2"] .tree-indent:after,
|
|
.tree-row[data-level="3"] .tree-indent:after,
|
|
.tree-row[data-level="4"] .tree-indent:after,
|
|
.tree-row[data-level="5"] .tree-indent:after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 7px;
|
|
top: 50%;
|
|
width: 12px;
|
|
border-top: 1px dotted #ccc;
|
|
}
|
|
|
|
/* 颜色标记 */
|
|
.leaf-node .layui-icon-file {
|
|
color: #5FB878;
|
|
}
|
|
|
|
.parent-node .layui-icon-triangle-d,
|
|
.parent-node .layui-icon-triangle-r {
|
|
color: #1E9FFF;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
}
|
|
|
|
// 导出Excel方法
|
|
exportToExcel(nodeId) {
|
|
// 显示加载层
|
|
const loadIndex = layer.load(2, {shade: [0.3, '#fff']});
|
|
|
|
// 请求导出API(传入任意节点ID,后端会忽略)
|
|
fetch(`/api/export/${nodeId}`)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('导出失败');
|
|
}
|
|
return response.blob();
|
|
})
|
|
.then(blob => {
|
|
// 创建下载链接
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = '工程知识图谱.xlsx';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
// 关闭加载层
|
|
layer.close(loadIndex);
|
|
layer.msg('导出成功', {icon: 1});
|
|
})
|
|
.catch(error => {
|
|
// 关闭加载层
|
|
layer.close(loadIndex);
|
|
layer.msg('导出失败: ' + error.message, {icon: 2});
|
|
});
|
|
}
|
|
|
|
renderRelationTable(relationships) {
|
|
table.render({
|
|
elem: '#relation-table-container',
|
|
data: relationships || [],
|
|
cols: [[
|
|
{type: 'numbers', title: '序号', width: 60},
|
|
{field: 'type', title: '关系类型', minWidth: 150},
|
|
{field: 'direction', title: '方向', width: 80, templet: d => {
|
|
return d.direction === 'OUTGOING' ? '出' : '入';
|
|
}},
|
|
{field: 'targetNodeId', title: '目标节点ID', minWidth: 150},
|
|
{field: 'targetNodeLabels', title: '目标节点类型', minWidth: 150},
|
|
{title: '操作', width: 80, templet: d => {
|
|
return '<button class="layui-btn layui-btn-xs" lay-event="navigate">查看</button>';
|
|
}}
|
|
]],
|
|
text: {
|
|
none: '该节点没有关系数据'
|
|
}
|
|
});
|
|
|
|
// 监听表格操作
|
|
table.on('tool(relation-table-container)', obj => {
|
|
if (obj.event === 'navigate') {
|
|
this.selectNode(obj.data.targetNodeId);
|
|
}
|
|
});
|
|
}
|
|
|
|
showNodeDetailLayer(data) {
|
|
let properties = {};
|
|
try {
|
|
if (data.allProperties) {
|
|
properties = JSON.parse(data.allProperties);
|
|
}
|
|
} catch (e) {
|
|
console.error('解析属性JSON失败:', e);
|
|
}
|
|
|
|
// 构建详情表格数据
|
|
const detailData = [
|
|
{key: 'ID', value: data.id},
|
|
{key: '节点类型', value: data.title}
|
|
];
|
|
|
|
// 添加所有属性
|
|
Object.entries(properties).forEach(([key, value]) => {
|
|
detailData.push({key, value: this.formatValue(value)});
|
|
});
|
|
|
|
// 创建弹出层
|
|
layer.open({
|
|
type: 1,
|
|
title: '节点详情',
|
|
area: ['600px', '500px'],
|
|
content: '<div id="detail-table-container" style="padding: 15px;"></div>',
|
|
success: () => {
|
|
// 渲染详情表格
|
|
table.render({
|
|
elem: '#detail-table-container',
|
|
data: detailData,
|
|
cols: [[
|
|
{field: 'key', title: '属性名', width: '30%'},
|
|
{field: 'value', title: '属性值', width: '70%'}
|
|
]],
|
|
skin: 'line',
|
|
even: true
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
formatValue(value) {
|
|
if (value === null || value === undefined) {
|
|
return '-';
|
|
}
|
|
if (typeof value === 'object') {
|
|
return JSON.stringify(value);
|
|
}
|
|
return String(value);
|
|
}
|
|
|
|
// 新增方法:渲染费用预览数据(包括费用集信息和默认表格)
|
|
renderCostPreviewData(costData) {
|
|
// 清除之前的费用集信息卡片
|
|
const costInfoContainer = document.getElementById('cost-info-container');
|
|
costInfoContainer.innerHTML = '';
|
|
|
|
// 重置按钮状态
|
|
document.getElementById('cost-item-btn').classList.add('layui-btn-disabled');
|
|
document.getElementById('material-machine-btn').classList.remove('layui-btn-disabled');
|
|
|
|
if (!costData || (!costData.cost_items && !costData.cost_sets)) {
|
|
// 如果没有费用数据,渲染空表格
|
|
this.renderCostPreviewTable(null);
|
|
return;
|
|
}
|
|
|
|
// 如果有CostSet信息,在表格上方显示
|
|
if (costData.cost_sets && costData.cost_sets.length > 0) {
|
|
const costSetInfo = document.createElement('div');
|
|
costSetInfo.className = 'layui-card';
|
|
costSetInfo.style.marginBottom = '10px';
|
|
costSetInfo.innerHTML = `
|
|
<div class="layui-card-header">费用集信息</div>
|
|
<div class="layui-card-body">
|
|
${costData.cost_sets.map(set => `
|
|
<p><strong>名称:</strong> ${set.name || '未命名费用集'}</p>
|
|
${set.properties && set.properties.GUID ? `<p><strong>GUID:</strong> ${set.properties.GUID}</p>` : ''}
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
|
|
costInfoContainer.appendChild(costSetInfo);
|
|
}
|
|
|
|
// 默认渲染CostItem类型的费用预览表格
|
|
this.renderCostPreviewTable(costData, 'CostItem');
|
|
}
|
|
|
|
// 新增方法:渲染费用预览表格
|
|
renderCostPreviewTable(costData, filterType = null, containerId = 'cost-preview-container') {
|
|
if (!costData || (!costData.cost_items && !costData.cost_sets)) {
|
|
// 如果没有费用数据,渲染空表格
|
|
table.render({
|
|
elem: `#${containerId}`,
|
|
data: [],
|
|
cols: [[
|
|
{type: 'numbers', title: '序号', width: 60},
|
|
{field: 'name', title: '费用项名称', minWidth: 200},
|
|
{field: 'cost', title: '费用', width: 120}
|
|
]],
|
|
skin: 'line',
|
|
even: true,
|
|
text: {
|
|
none: '该节点没有费用预览数据'
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 准备费用预览表格数据
|
|
let costItems = costData.cost_items || [];
|
|
|
|
// 如果指定了过滤类型,只显示该类型的费用项
|
|
if (filterType) {
|
|
costItems = costItems.filter(item => {
|
|
const itemType = item.labels && item.labels.length > 0 ? item.labels[0] : '';
|
|
return itemType === filterType;
|
|
});
|
|
}
|
|
|
|
// 处理费用项数据
|
|
const tableData = costItems.map(item => {
|
|
const props = item.properties || {};
|
|
const itemType = item.labels && item.labels.length > 0 ? item.labels[0] : '';
|
|
|
|
// 基本数据结构
|
|
const tableItem = {
|
|
id: item.id,
|
|
name: props.name || '未命名费用项',
|
|
type: props.type || itemType
|
|
};
|
|
|
|
// 根据节点类型添加不同的属性
|
|
if (itemType === 'MaterialandmachineCostItem') {
|
|
// 材料机械费用项
|
|
tableItem.supply = props.供货方 || '';
|
|
tableItem.code = props.编码 || '';
|
|
tableItem.unit = props.单位 || '';
|
|
tableItem.amount = props.数量 || 0;
|
|
tableItem.budgetPrice = props.预算价不含税 || 0;
|
|
tableItem.marketPrice = props.市场价不含税 || 0;
|
|
tableItem.totalBudget = props.预算价合价 || 0;
|
|
tableItem.totalMarket = props.市场价合价 || 0;
|
|
tableItem.priceDiff = props.价差 || 0;
|
|
} else if (itemType === 'CostItem') {
|
|
// 普通费用项
|
|
tableItem.cost = props.cost || 0;
|
|
tableItem.unique_id = props.unique_id || '';
|
|
} else {
|
|
// 其他类型的费用项
|
|
if (props.amount !== undefined) tableItem.amount = props.amount;
|
|
if (props.unit !== undefined) tableItem.unit = props.unit;
|
|
if (props.unitPrice !== undefined) tableItem.unitPrice = props.unitPrice;
|
|
if (props.totalPrice !== undefined) tableItem.totalPrice = props.totalPrice;
|
|
if (props.cost !== undefined) tableItem.cost = props.cost;
|
|
}
|
|
|
|
return tableItem;
|
|
});
|
|
|
|
// 获取适合的列配置
|
|
let columnConfig;
|
|
if (filterType && this.columnConfigs[filterType]) {
|
|
// 使用指定类型的列配置
|
|
columnConfig = this.columnConfigs[filterType].costPreview;
|
|
} else if (tableData.length > 0) {
|
|
// 根据第一个项目的类型决定使用哪种列配置
|
|
const firstItemType = tableData[0].type;
|
|
if (this.columnConfigs[firstItemType]) {
|
|
columnConfig = this.columnConfigs[firstItemType].costPreview;
|
|
} else {
|
|
columnConfig = this.columnConfigs['default'].costPreview;
|
|
}
|
|
} else {
|
|
columnConfig = this.columnConfigs['default'].costPreview;
|
|
}
|
|
|
|
// 构建列配置
|
|
const cols = [
|
|
{type: 'numbers', title: '序号', width: 60}
|
|
];
|
|
|
|
// 添加可见的列
|
|
columnConfig.forEach(col => {
|
|
if (col.show) {
|
|
cols.push({
|
|
field: col.field,
|
|
title: col.title,
|
|
minWidth: col.minWidth || col.width || 100,
|
|
width: col.width
|
|
});
|
|
}
|
|
});
|
|
|
|
// 渲染费用预览表格
|
|
table.render({
|
|
elem: `#${containerId}`,
|
|
data: tableData,
|
|
cols: [cols],
|
|
skin: 'line',
|
|
even: true,
|
|
text: {
|
|
none: filterType === 'CostItem' ?
|
|
'该节点没有费用预览数据' :
|
|
'该节点没有材机列表数据'
|
|
}
|
|
});
|
|
}
|
|
|
|
buildTreeStructureData(node, children) {
|
|
// 处理子节点数据
|
|
const treeData = [];
|
|
|
|
// 处理子节点数据
|
|
for (const child of children) {
|
|
// 增强过滤条件,过滤掉费用预览相关节点
|
|
if (
|
|
(child.name && child.name.includes('费用预览')) ||
|
|
(child.properties && child.properties.name && child.properties.name.includes('费用预览')) ||
|
|
(child.labels && child.labels.some(label => label.includes('Cost'))) ||
|
|
(child.type && child.type.includes('Cost'))
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
const item = {
|
|
id: child.id,
|
|
children: []
|
|
};
|
|
|
|
// 添加属性
|
|
if (child.properties) {
|
|
// 添加标准属性
|
|
item.code = child.properties.code || '';
|
|
item.name = child.properties.name || '';
|
|
item.specification = child.properties.specification || child.properties.规格型号 || '';
|
|
item.unit = child.properties.unit || child.properties.单位 || '';
|
|
item.formula = child.properties.formula || child.properties.计算式 || '';
|
|
item.amount = child.properties.amount || child.properties.数量 || '';
|
|
item.unitPriceWithTax = child.properties.unitPriceWithTax || child.properties.单价含税 || '';
|
|
item.unitPriceWithoutTax = child.properties.unitPriceWithoutTax || child.properties.单价不含税 || '';
|
|
item.totalPriceWithTax = child.properties.totalPriceWithTax || child.properties.合价含税 || '';
|
|
item.totalPriceWithoutTax = child.properties.totalPriceWithoutTax || child.properties.合价不含税 || '';
|
|
item.taxIndex = child.properties.taxIndex || child.properties.取费索引 || '';
|
|
item.remark = child.properties.remark || child.properties.备注 || '';
|
|
|
|
// 将所有属性序列化为JSON存储,用于详情查看
|
|
item.allProperties = JSON.stringify(child.properties);
|
|
}
|
|
|
|
// 处理子节点
|
|
if (child.children && child.children.length > 0) {
|
|
item.children = this.buildTreeStructureData(child, child.children);
|
|
}
|
|
|
|
treeData.push(item);
|
|
}
|
|
|
|
return treeData;
|
|
}
|
|
|
|
buildTreeStructureHTML(treeData, level = 0) {
|
|
let html = '';
|
|
|
|
// 添加表头行
|
|
if (level === 0) {
|
|
html += `<tr class="tree-header">
|
|
<th class="tree-col-name">项目名称</th>
|
|
<th class="tree-col-spec">规格型号</th>
|
|
<th class="tree-col-unit">单位</th>
|
|
<th class="tree-col-formula">计算式</th>
|
|
<th class="tree-col-amount">数量</th>
|
|
<th class="tree-col-price">单价含税</th>
|
|
<th class="tree-col-price">单价不含税</th>
|
|
<th class="tree-col-total">合价含税</th>
|
|
<th class="tree-col-total">合价不含税</th>
|
|
<th class="tree-col-tax">取费表</th>
|
|
<th class="tree-col-remark">备注</th>
|
|
</tr>`;
|
|
}
|
|
|
|
// 生成行数据
|
|
treeData.forEach((item, index) => {
|
|
const hasChildren = item.children && item.children.length > 0;
|
|
const toggleClass = hasChildren ? 'tree-toggle' : '';
|
|
const toggleIcon = hasChildren ? '<i class="layui-icon layui-icon-triangle-d"></i>' : '<i class="layui-icon layui-icon-file"></i>';
|
|
const nodeClass = hasChildren ? 'parent-node' : 'leaf-node';
|
|
|
|
// 创建行
|
|
html += `<tr class="tree-row ${nodeClass}" data-id="${item.id}" data-level="${level}">`;
|
|
|
|
// 项目名称列(带缩进和图标)
|
|
html += `<td class="tree-col-name">
|
|
<div class="tree-indent" style="padding-left: ${level * 20}px">
|
|
<span class="${toggleClass}">${toggleIcon}</span>
|
|
<span class="node-code">${item.code ? item.code : ''}</span>
|
|
<span class="node-name">${item.name || '未命名项目'}</span>
|
|
</div>
|
|
</td>`;
|
|
|
|
// 规格型号列
|
|
html += `<td class="tree-col-spec">${item.specification || ''}</td>`;
|
|
|
|
// 单位列
|
|
html += `<td class="tree-col-unit">${item.unit || ''}</td>`;
|
|
|
|
// 计算式列
|
|
html += `<td class="tree-col-formula">${item.formula || ''}</td>`;
|
|
|
|
// 数量列
|
|
html += `<td class="tree-col-amount">${item.amount || ''}</td>`;
|
|
|
|
// 单价含税列
|
|
html += `<td class="tree-col-price">${item.unitPriceWithTax || ''}</td>`;
|
|
|
|
// 单价不含税列
|
|
html += `<td class="tree-col-price">${item.unitPriceWithoutTax || ''}</td>`;
|
|
|
|
// 合价含税列
|
|
html += `<td class="tree-col-total">${item.totalPriceWithTax || ''}</td>`;
|
|
|
|
// 合价不含税列
|
|
html += `<td class="tree-col-total">${item.totalPriceWithoutTax || ''}</td>`;
|
|
|
|
// 取费表列
|
|
html += `<td class="tree-col-tax">${item.taxIndex || ''}</td>`;
|
|
|
|
// 备注列
|
|
html += `<td class="tree-col-remark">${item.remark || ''}</td>`;
|
|
|
|
html += '</tr>';
|
|
|
|
// 递归处理子节点
|
|
if (hasChildren) {
|
|
html += this.buildTreeStructureHTML(item.children, level + 1);
|
|
}
|
|
});
|
|
|
|
return html;
|
|
}
|
|
|
|
addTreeStructureStyles() {
|
|
// 检查是否已经添加了样式
|
|
if (!document.getElementById('tree-structure-styles')) {
|
|
const style = document.createElement('style');
|
|
style.id = 'tree-structure-styles';
|
|
style.textContent = `
|
|
.project-division-tree {
|
|
width: 100%;
|
|
overflow: auto;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.project-division-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
table-layout: fixed;
|
|
}
|
|
|
|
.project-division-table tr {
|
|
border-bottom: 1px solid #e6e6e6;
|
|
height: 40px;
|
|
}
|
|
|
|
.project-division-table td, .project-division-table th {
|
|
padding: 8px;
|
|
vertical-align: middle;
|
|
border-right: 1px solid #e6e6e6;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.tree-header {
|
|
background-color: #f2f2f2;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.tree-col-name {
|
|
width: 200px;
|
|
text-align: left;
|
|
}
|
|
|
|
.tree-col-spec {
|
|
width: 100px;
|
|
text-align: center;
|
|
}
|
|
|
|
.tree-col-unit {
|
|
width: 60px;
|
|
text-align: center;
|
|
}
|
|
|
|
.tree-col-formula {
|
|
width: 100px;
|
|
text-align: center;
|
|
}
|
|
|
|
.tree-col-amount {
|
|
width: 80px;
|
|
text-align: right;
|
|
}
|
|
|
|
.tree-col-price {
|
|
width: 100px;
|
|
text-align: right;
|
|
}
|
|
|
|
.tree-col-total {
|
|
width: 120px;
|
|
text-align: right;
|
|
}
|
|
|
|
.tree-col-tax {
|
|
width: 100px;
|
|
text-align: center;
|
|
}
|
|
|
|
.tree-col-remark {
|
|
width: 100px;
|
|
text-align: left;
|
|
}
|
|
|
|
.tree-indent {
|
|
display: flex;
|
|
align-items: center;
|
|
position: relative;
|
|
}
|
|
|
|
.tree-toggle {
|
|
cursor: pointer;
|
|
margin-right: 8px;
|
|
display: inline-block;
|
|
width: 16px;
|
|
height: 16px;
|
|
text-align: center;
|
|
line-height: 16px;
|
|
}
|
|
|
|
.node-code {
|
|
color: #999;
|
|
font-size: 12px;
|
|
margin-right: 5px;
|
|
}
|
|
|
|
.node-name {
|
|
font-weight: normal;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.parent-node .node-name {
|
|
font-weight: bold;
|
|
color: #1E9FFF;
|
|
}
|
|
|
|
.tree-row:hover {
|
|
background-color: #f2f2f2;
|
|
}
|
|
|
|
/* 树状连接线样式 */
|
|
.tree-row .tree-indent {
|
|
position: relative;
|
|
}
|
|
|
|
.tree-row[data-level="1"] .tree-indent:before,
|
|
.tree-row[data-level="2"] .tree-indent:before,
|
|
.tree-row[data-level="3"] .tree-indent:before,
|
|
.tree-row[data-level="4"] .tree-indent:before,
|
|
.tree-row[data-level="5"] .tree-indent:before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 7px;
|
|
top: -8px;
|
|
height: calc(100% + 16px);
|
|
border-left: 1px dotted #ccc;
|
|
}
|
|
|
|
.tree-row[data-level="1"] .tree-indent:after,
|
|
.tree-row[data-level="2"] .tree-indent:after,
|
|
.tree-row[data-level="3"] .tree-indent:after,
|
|
.tree-row[data-level="4"] .tree-indent:after,
|
|
.tree-row[data-level="5"] .tree-indent:after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 7px;
|
|
top: 50%;
|
|
width: 12px;
|
|
border-top: 1px dotted #ccc;
|
|
}
|
|
|
|
/* 颜色标记 */
|
|
.leaf-node .layui-icon-file {
|
|
color: #5FB878;
|
|
}
|
|
|
|
.parent-node .layui-icon-triangle-d,
|
|
.parent-node .layui-icon-triangle-r {
|
|
color: #1E9FFF;
|
|
}
|
|
|
|
/* 费用预览按钮样式 */
|
|
.cost-preview-btn {
|
|
padding: 0 10px;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
}
|
|
|
|
async getAllChildrenRecursively(children) {
|
|
const allChildren = [...children];
|
|
|
|
for (const child of allChildren) {
|
|
if (child.id) {
|
|
try {
|
|
// 获取子节点的子节点
|
|
const response = await fetch(`/api/node/${child.id}`);
|
|
const data = await response.json();
|
|
|
|
if (data.children && data.children.length > 0) {
|
|
// 递归获取子节点的子节点
|
|
const grandChildren = await this.getAllChildrenRecursively(data.children);
|
|
|
|
// 将子节点的子节点添加到子节点的children属性中
|
|
child.children = grandChildren;
|
|
}
|
|
} catch (error) {
|
|
console.error('获取子节点失败:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
return allChildren;
|
|
}
|
|
}
|
|
|
|
// 初始化应用
|
|
new KnowledgeGraphUI();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |