Уважаемые эксперты, прошу помощи.
Задача стоит следующим образом:
Есть таблицы в БД, мне нужно нарисовать кастомную ER (entity relation) диаграмму связей в БД. Кастомную в связи со спецификой продукта в БД.
Для управления приложением используется JavaFX и контроллер WebView.
Идея в том, что на Java идет работа с чтением БД, а при действиях пользователя я буду динамически добавлять таблицы на диаграмму (веб-страницу)
К сожалению, высоким уровнем компетенции на JS+CSS не обладаю, в связи с чем обратился к GPT но дошел до тупика.
GPT рекомендовал использовать jsPlumb. Из разных вариантов, что он предлагал, этот показался более менее читаемым.
Вот что я имею сейчас:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>ER Диаграмма с jsPlumb</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsPlumb/2.15.6/js/jsplumb.min.js"></script>
<style>
body { position: relative; width: 100%; height: 100%; margin: 0; overflow: hidden; }
.table {
width: 150px;
padding: 10px;
border: 1px solid #000;
border-radius: 5px;
background: #fff;
position: absolute;
cursor: move;
box-shadow: 2px 2px 5px rgba(0,0,0,0.3);
}
.attribute {
padding-left: 10px;
cursor: default; /* Отключаем курсор для атрибутов */
user-select: none; /* Отключаем выделение текста */
}
.aLabel {
font-size: 12px;
background: rgba(255,255,255,0.8);
padding: 2px;
border-radius: 3px;
}
</style>
</head>
<body>
<div id="diagram-container" style="width: 100%; height: 100%; position: relative;"></div>
<script>
jsPlumb.ready(function() {
const instance = jsPlumb.getInstance({
Connector: "Bezier",
Anchors: ["Right", "Left"],
Endpoint: ["Dot", { radius: 5 }],
PaintStyle: { stroke: "#999", strokeWidth: 2 },
EndpointStyle: { fill: "#999" },
Container: "diagram-container"
});
let allowConnection = false; // Флаг для разрешения соединений через код
// Обработчик перед созданием соединения
instance.bind("beforeDrop", function(info) {
if (allowConnection) {
allowConnection = false; // Сброс флага после разрешения
return true; // Разрешить соединение
}
return false; // Запретить соединение, созданное пользователем
});
// Функция для добавления таблицы
window.addTable = function(id, name, x, y, attributes) {
if (document.getElementById(id)) {
console.warn(`Таблица с id "${id}" уже существует.`);
return;
}
const table = document.createElement("div");
table.className = "table";
table.id = id;
table.style.left = x + "px";
table.style.top = y + "px";
const title = document.createElement("strong");
title.innerText = name;
table.appendChild(title);
attributes.forEach(attr => {
const attrDiv = document.createElement("div");
attrDiv.className = "attribute";
attrDiv.id = attr.id;
attrDiv.innerText = attr.name;
table.appendChild(attrDiv);
// Настройка источника и цели для атрибутов
instance.makeSource(attrDiv, {
anchor: "Continuous",
connectorStyle: { stroke: "#999", strokeWidth: 2 },
connectionType: "basic",
extract: { "action": "the-action" },
maxConnections: -1,
filter: ".attribute",
allowLoopback: false
});
instance.makeTarget(attrDiv, {
anchor: "Continuous",
allowLoopback: false,
maxConnections: -1,
dropOptions: { hoverClass: "dragHover" }
});
});
document.getElementById("diagram-container").appendChild(table);
// Сделать таблицу перетаскиваемой
instance.draggable(table, {
grid: [10, 10],
stop: function() {
instance.repaintEverything();
}
});
};
// Функция для добавления связи
window.addConnection = function(sourceId, targetId, type) {
const sourceElem = document.getElementById(sourceId);
const targetElem = document.getElementById(targetId);
if (!sourceElem || !targetElem) {
console.warn(`Элемент(ы) с id "${sourceId}" или "${targetId}" не найдены.`);
return;
}
allowConnection = true; // Разрешить соединение через код
instance.connect({
source: sourceId,
target: targetId,
overlays: [
["Label", { label: type, location: 0.5, id: "label", cssClass: "aLabel" }]
]
});
};
// Отключаем возможность добавления соединений пользователем
// Можно дополнительно скрыть эндпоинты, если необходимо
addTable('users', 'Users', 100, 100, [
{ id: 'u_id', name: 'id' },
{ id: 'u_name', name: 'name' },
{ id: 'u_email', name: 'email' }
]);
addTable('articles', 'Articles', 400, 100, [
{ id: 'a_id', name: 'id' },
{ id: 'a_title', name: 'title' },
{ id: 'a_content', name: 'content' },
{ id: 'a_author_id', name: 'author_id' }
]);
addConnection('u_id', 'a_author_id', '1:M');
});
</script>
</body>
</html>
С чем я прошу помощи:
Условия:
- нужно запретить пользователю редактировать связи (сейчас если потянуть за точку вначале связи, ее можно убрать или подвинуть)
- нужно сделать так, чтобы узел таблицы были оформлены именно как таблицы (сейчас это оформлено просто как облачко). Колонку можно для теста добавить любую
Связи при этом должно отходить именно от строки таблицы, а не от всего узла целиком
- нужно так же сделать возможность указывать связь саму на себя (т.е. например таблицу user может иметь поле "замещающий пользователь", в котором будет ссылка на эту же таблицу. в Этом случае от поля substitute_user связь должна отходить на колонку id
если кто-то знает менее трудоемкие инструменты для решения моей задачи, укажите пожалуйста.
Благодарю за внимание.