Files
KG_generation/templates/html_template.html
T

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>