В статье рассматривается способ отображения иерархических структур пользователю в HTML-отчет с помощью библиотеки JointJS. Эта библиотека позволяет автоматически располагать элементы иерархии на схеме и автоматически прокладывать связи между блоками. Я ее использовал для отображения схемы сложных настраиваемых динамических бизнес-процессов, где без автоматического расположения никак не обойтись. Одна из заявок на awards напомнила мне, что я когда-то хотел с помощью этой библиотеки сделать аналог вот этого решения. Пока что решил сделать универсальный шаблон, с помощью которого не сложно вывести пользователю любую иерархию. В сценарии, как примеры, реализованы простенькие отчеты иерархии групп, подразделений и замещений.
Так как расположение блоков вычисляется каждый раз при открытии схемы, то при большом количестве блоков данный подход будет работать медленно. Если блоков много, то стоит отказаться от использования автоматического расположения в пользу задания расположения блоков в SQL-запросе на основе уровня иерархии, порядкового номера и количества дочерних элементов. Для некоторых задач также подойдет вариант рассчитывать положение только в первый раз и сохранять JSON с разметкой для JointJS.
Я собрал все необходимые JS-библиотеки в один файл и вместе c CSS выложил на cdn.rawgit.com. Для тестирования этого достаточно, для промышленного использования библиотеки надо разместить у себя. Я рекомендую пользоваться вот этим решением (в следующей статье я покажу как и почему удобно использовать это решение для веб-контролов и отчетов в вебе) или просто разместить файлы в папке веб-доступа.
// SQL-запрос, формирующий XML(потом преобразуется в JSON) в формате, принимаемом JointJS
DataQuery = "
with hierarchy(ID, MainID, Name) as (
select Analit, Podr, NameAn
from MBAnalit
where Podr is null
and Sost = 'Д'
and Vid = " & References.ПОД.ID & "
union all
select dep.Analit, dep.Podr, dep.NameAn
from MBAnalit dep
join hierarchy on dep.Podr = hierarchy.ID
where dep.Vid = " & References.ПОД.ID & "
and dep.Sost = 'Д')
select cast((
select
( select 'cell-' + cast(ID as varchar) as [id]
, 'basic.Rect' as [type]
, Name as [attrs/text/text]
from hierarchy
order by Name asc
for xml path('cells'), elements, type)
, (select 'link-' + cast(MainID as varchar) + '-to-' + cast(ID as varchar) as [id]
, 'link' as [type]
, 'cell-' + cast(MainID as varchar) as [source/id]
, 'cell-' + cast(ID as varchar) as [target/id]
from hierarchy
where MainID is not null
for xml path('cells'), elements, type)
for xml path('data')
) as varchar(max))"
DataXml = SQL(DataQuery)
HTML = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta content="text/html; charset=CP1251" http-equiv="content-type">
<meta http-equiv="X-UA-Compatible" content="IE=edge;" />
<title>Иерархия подразделений</title>
<link rel="stylesheet" type="text/css" href="https://cdn.rawgit.com/Chugunenok/ClubDirectumFiles/7ebf0908/hierarchicalStructure.min.css" />
<script src="https://cdn.rawgit.com/Chugunenok/ClubDirectumFiles/f30ec122/hierarchicalStructure.min.js"></script>
<script type="text/javascript">
var x2js = new X2JS();
var dataXML = "' & DataXml & '"; // строка XML с данными
var dataJSON = x2js.xml_str2json(dataXML).data; // преобразуем в JSON и получаем массив cells
var graph = new joint.dia.Graph();
$(document).ready(function () {
// Добавляем статичные параметры, одинаковые для всех блоков и связей
dataJSON.cells.forEach(function (currentValue, index, array){
if(currentValue.type == "link"){
currentValue.router = { // Маршрутизация связей https://resources.jointjs.com/demos/routing
name: "manhattan",
args: {
startDirections: ["right"],
endDirections: ["left"],
step: 5,
maximumLoops: 1000,
excludeTypes: ["basic.Rect"]
}
};
currentValue.connector = { name: "rounded", args: { radius: "10" }};
}
// SVG не умеет автоматом переносить текст, поэтому делаем это вручную,
// попутно устанавливая высоту блоков по количеству строк
if(currentValue.type == "basic.Rect"){
currentValue.attrs.text.text = joint.util.breakText(currentValue.attrs.text.text,
{ width: 150 }, { "font-size": "1em"});
var height = currentValue.attrs.text.text.split(/\r\n|\r|\n/).length * 20;
currentValue.size = { width: 150, height: height};
}
});
var paper = new joint.dia.Paper({
el: $("#paper"),
gridSize: 5,
model: graph
});
graph.fromJSON(dataJSON);
// Выполняем автоматическое расположение блоков https://resources.jointjs.com/demos/layout
// Полный список параметров и описание https://github.com/dagrejs/dagre/wiki
joint.layout.DirectedGraph.layout(graph, {
rankSep: 30, // Расстояние между уровнями
rankDir: "LR", // Направление схемы. LR = Left to Right
edgeSep: 5,
nodeSep: 15, // Расстояние между блоками одного уровнями
marginX: 30, // Отступ всей схемы от краев
marginY: 30,
align: "UL" // Приоритетное положение блоков. UL = Upper Left
});
// Установим размер контейнера схемы по размеру содержимого
mainSVGElemBBox = document.querySelector("svg > g").getBBox();
paper.setDimensions(mainSVGElemBBox.width*1.05 + 40, mainSVGElemBBox.height*1.05 + 40);
});
</script>
</head>
<body>
<div id="paper" class="paper"></div>
</body>
</html>'
FilePath = GetTempFolder() & 'Structure.html'
WriteFile(FilePath;; HTML)
ExecuteProcess('explorer "' & FilePath & '"')
Если данный материал интересен сообществу, то я могу немного доработать шаблон и/или доработать/сделать новый пример.
Что не очень сложно сделать:
Есть еще идея сделать отчет, показывающий у кого есть права на документ с учетом иерархии групп, вхождения пользователя в группы и иерархии замещений. Но это сделать сложновато, так что только если будет большой спрос сделаю.
JS и CSS: исходные библиотеки и объединенный файл.
Использованные библиотеки:
Сценарий с примером (импорт через компоненту "Сценарии")
Довести решение до приличного состояния нет времени, поэтому пока выложу что и как можно сделать.
.marker-arrowhead, .tool-remove, .tool-options, .marker-vertices, .connection-wrap {
display: none;
}
.connection {
fill:none;
}
В JS добавляем функцию, которую надо вызывать когда надо сохранить схему
function saveGraph(fileType) // fileType: png, pdf, jpeg
{
var svg = document.querySelector("svg");
var fileName = "scheme." + fileType;
SVG2Bitmap(svg, function (canvas, dataURI) {
if (fileType == "pdf" || fileType == "jpeg") {
var converter = document.createElement("canvas");
converter.width = canvas.width;
converter.height = canvas.height;
ctx = converter.getContext("2d")
ctx.fillStyle = "#FFFF"; /// заполним прозрачное белым
ctx.fillRect(0, 0, converter.width, converter.height);
ctx.drawImage(canvas, 0, 0);
dataURI = converter.toDataURL("image/jpeg", 0.5);
}
if (fileType == "pdf") {
var doc = new jsPDF(canvas.height > canvas.width ? "p" : "l", "px", [canvas.height, canvas.width], true);
doc.addImage(dataURI, "JPEG", 0, 0, canvas.width, canvas.height, "scheme", "FAST");
doc.save(fileName);
} else {
var link = document.createElement("a");
link.download = fileName;
link.href = dataURI;
link.click();
}
}, { type: "image/png", scale: "3" });
}
Задание цвета для блоков. В SQL-запрос, формирующий XML, добавляем 2 колонки с цветом границы [attrs/rect/stroke] и цветом заливки [attrs/rect/fill], например, 'orange' as [attrs/rect/stroke] и 'white' as [attrs/rect/fill].
Открытие записи справочника при клике на блок. в SQL-запрос, формирующий XML, добавляем колонку с именем типа справочника блока, например, 'ПОЛ' as [referenceType]. В JS в самый конец $(document).ready() добавляем
paper.on("cell:pointerclick",
function(cellView, evt, x, y) {
var refType = cellView.model.attributes.referenceType;
if(refType){
var id = cellView.model.id.replace("cell-","");
window.open("http://' & SystemSetting("MB_AWebServerName") & '/reference.asp?sys=' & Application.Connection.SystemInfo.Code & '&compcode=" + refType + "&id=" + id);
}
}
);
Задание всплывающей подсказки (hint) при наведении на блок. Используется библиотека qTip2 с плагинами Viewport и SVG и набором встроенных стилей.
В SQL-запрос, формирующий XML, добавляем колонку [hint]. В JS в самый конец $(document).ready() добавляем
$(".joint-cell.joint-element").qtip({
content: { text: function(event) {
var modelId = event.currentTarget.attributes["model-id"].value;
var cell = graph.getCell(modelId);
return cell.attributes.hint;
}
},
hide: {
fixed: true,
delay: 300
},
position: {
at: "top right",
my: "top left",
effect: false,
viewport: $("body"),
adjust: {
method: "shift flip"
}
},
style: {
classes: "qtip-blue"
}
});
.highlighted-link {
stroke-width: 5 !important;
stroke: orange !important;
}
.highlighted-link .connection-wrap {
display: block;
}
В JS в самый конец $(document).ready() добавляем
var highlightedCellViews = [];
var linkHighlighter = {highlighter: {
name: "addClass",
options: {
className: "highlighted-link"
}
}};
paper.on("cell:mouseover",
function(cellView, evt, x, y) {
var highlightLinksToParents = function(model){
var connections = graph.getConnectedLinks(model, { inbound: true });
connections.forEach(function(link){
var cellViewToHighlight = paper.findViewByModel(link);
cellViewToHighlight.highlight(null, linkHighlighter);
link.toFront();
highlightedCellViews.push(cellViewToHighlight);
var parent = graph.getCell(link.attributes.source.id);
highlightLinksToParents(parent);
});
}
highlightLinksToParents(cellView.model);
}
);
paper.on("cell:mouseout", function() {
while(highlightedCellViews.length > 0) {
highlightedCellViews.pop().unhighlight(null, linkHighlighter);
}
}
);
Задание дополнительного текста для блока. Мелким шрифтом под основным текстом. Под каждую задачу можно создать свой тип блока (или расширить стандартный) или использовать уже готовые типы блоков библиотеки JointJS. Например, для организационной диаграммы можно использовать готовый тип блока org.Member (нужно будет добавить JS плагина).
Все новые библиотеки добавлены в общий файл hierarchicalStructure.min.js, стили qTip добавлены в hierarchicalStructure.min.css.
Пример (на лучшее времени пока нет) как это может выглядеть:
Не знаю когда появится время сделать полноценные удобные и красивые отчеты, поэтому пока что добавил в статью как сделать (с примерами кода):
Если что-то непонятно, сделано неправильно или знаете как сделать лучше\проще, обязательно пишите!
Добрый день, не могу найти и скачать файлы, необходимые для работы сценария. Подскажите пожалуйста что нужно сделать.
Добрый день.
Файлы есть тут: hierarchicalStructure.min.js, hierarchicalStructure.min.css
и тут: JS и CSS: исходные библиотеки и объединенный файл.
Авторизуйтесь, чтобы написать комментарий