Files
LibraryAPI/library_service/templates/api.html

303 lines
12 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title id="pageTitle">Loading...</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/dagre/0.8.5/dagre.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsPlumb/2.15.6/js/jsplumb.min.js"></script>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
color: #333;
}
h1 {
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}
ul {
list-style: none;
display: flex;
padding: 0;
margin: 0;
gap: 20px;
flex-wrap: wrap;
}
li { margin: 10px 0; }
a {
display: inline-block;
padding: 8px 15px;
background-color: #3498db;
color: white;
text-decoration: none;
border-radius: 4px;
transition: background-color 0.3s;
font-size: 14px;
}
a:hover { background-color: #2980b9; }
p { margin: 5px 0; }
.status-ok { color: #27ae60; }
.status-error { color: #e74c3c; }
.server-time { color: #7f8c8d; font-size: 12px; }
#erDiagram {
position: relative;
width: 100%; height: 700px;
border: 1px solid #ddd;
border-radius: 8px;
margin-top: 30px;
background: #fcfcfc;
background-image:
linear-gradient(#eee 1px, transparent 1px),
linear-gradient(90deg, #eee 1px, transparent 1px);
background-size: 20px 20px;
background-position: -1px -1px;
overflow: hidden;
cursor: grab;
}
#erDiagram:active { cursor: grabbing; }
.er-table {
position: absolute;
width: 200px;
background: #fff;
border: 1px solid #bdc3c7;
border-radius: 5px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
font-size: 12px;
z-index: 10;
display: flex;
flex-direction: column;
}
.er-table-header {
background: #3498db;
color: #ecf0f1;
padding: 8px;
font-weight: bold;
text-align: center;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.er-table-body {
background: #fff;
padding: 4px 0;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.er-field {
padding: 4px 10px;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.er-field:hover {
background-color: #ecf0f1;
color: #2980b9;
}
.relation-label {
font-size: 10px;
font-weight: bold;
background: white;
padding: 2px 4px;
border: 1px solid #bdc3c7;
border-radius: 3px;
color: #7f8c8d;
z-index: 20;
}
.jtk-connector { z-index: 5; }
.jtk-endpoint { z-index: 5; }
</style>
</head>
<body>
<img src="/favicon.ico" />
<h1 id="mainTitle">Загрузка...</h1>
<p>Версия: <span id="appVersion">-</span></p>
<p>Описание: <span id="appDescription">-</span></p>
<p>Статус: <span id="appStatus">-</span></p>
<p class="server-time">Время сервера: <span id="serverTime">-</span></p>
<ul>
<li><a href="/">Главная</a></li>
<li><a href="/docs">Swagger UI</a></li>
<li><a href="/redoc">ReDoc</a></li>
<li><a href="https://github.com/wowlikon/LiB">Исходный код</a></li>
</ul>
<h2>Интерактивная ER диаграмма</h2>
<div id="erDiagram"></div>
<script>
async function fetchInfo() {
try {
const response = await fetch('/api/info');
const data = await response.json();
document.getElementById('pageTitle').textContent = data.app_info.title;
document.getElementById('mainTitle').textContent = `Добро пожаловать в ${data.app_info.title} API!`;
document.getElementById('appVersion').textContent = data.app_info.version;
document.getElementById('appDescription').textContent = data.app_info.description;
const statusEl = document.getElementById('appStatus');
statusEl.textContent = data.status;
statusEl.className = data.status === 'ok' ? 'status-ok' : 'status-error';
document.getElementById('serverTime').textContent = new Date(data.server_time).toLocaleString();
} catch (error) {
console.error('Ошибка загрузки info:', error);
document.getElementById('appStatus').textContent = 'Ошибка соединения';
document.getElementById('appStatus').className = 'status-error';
}
}
async function fetchSchemaAndRender() {
try {
const response = await fetch('/api/schema');
const diagramData = await response.json();
renderDiagram(diagramData);
} catch (error) {
console.error('Ошибка загрузки схемы:', error);
document.getElementById('erDiagram').innerHTML = '<p style="padding:20px;color:#e74c3c;">Ошибка загрузки схемы</p>';
}
}
function renderDiagram(diagramData) {
jsPlumb.ready(function () {
const instance = jsPlumb.getInstance({
Container: "erDiagram",
Connector: ["Flowchart", { stub: 30, gap: 10, cornerRadius: 5, alwaysRespectStubs: true }],
ConnectionOverlays: [["Arrow", { location: 1, width: 10, length: 10, foldback: 0.8 }]]
});
const container = document.getElementById("erDiagram");
const tableWidth = 200;
const g = new dagre.graphlib.Graph();
g.setGraph({
nodesep: 60, ranksep: 80,
marginx: 20, marginy: 20,
rankdir: 'LR',
});
g.setDefaultEdgeLabel(() => ({}));
const fieldIndexByEntity = {};
diagramData.entities.forEach(entity => {
const idxMap = {};
entity.fields.forEach((field, idx) => { idxMap[field.id] = idx; });
fieldIndexByEntity[entity.id] = idxMap;
});
diagramData.entities.forEach(entity => {
const table = document.createElement("div");
table.className = "er-table";
table.id = "table-" + entity.id;
const header = document.createElement("div");
header.className = "er-table-header";
header.textContent = entity.title || entity.id;
const body = document.createElement("div");
body.className = "er-table-body";
entity.fields.forEach(field => {
const row = document.createElement("div");
row.className = "er-field";
row.id = `field-${entity.id}-${field.id}`;
row.style.display = "flex";
row.style.alignItems = "center";
const labelSpan = document.createElement("span");
labelSpan.textContent = field.label || field.id;
row.appendChild(labelSpan);
if (field.tooltip) {
row.title = field.tooltip;
const tip = document.createElement("span");
tip.textContent = "ⓘ";
tip.title = field.tooltip;
tip.style.marginLeft = "4px";
tip.style.marginRight = "0";
tip.style.fontSize = "10px";
tip.style.cursor = "help";
tip.style.marginLeft = "auto";
row.appendChild(tip);
}
body.appendChild(row);
});
table.appendChild(header);
table.appendChild(body);
container.appendChild(table);
const estimatedHeight = 20 + (entity.fields.length * 26);
g.setNode(entity.id, { width: tableWidth, height: estimatedHeight });
});
const layoutEdges = [];
const m2oGroups = {};
diagramData.relations.forEach(rel => {
const isManyToOne = (rel.fromMultiplicity === 'N' || rel.fromMultiplicity === '*') && (rel.toMultiplicity === '1');
if (isManyToOne) {
const fi = (fieldIndexByEntity[rel.fromEntity] || {})[rel.fromField] ?? 0;
if (!m2oGroups[rel.fromEntity]) m2oGroups[rel.fromEntity] = [];
m2oGroups[rel.fromEntity].push({ rel, fieldIndex: fi });
} else {
layoutEdges.push({ source: rel.fromEntity, target: rel.toEntity });
}
});
Object.keys(m2oGroups).forEach(fromEntity => {
const arr = m2oGroups[fromEntity];
arr.sort((a, b) => a.fieldIndex - b.fieldIndex);
arr.forEach((item, idx) => {
const rel = item.rel;
if (idx % 2 === 0) { layoutEdges.push({ source: rel.toEntity, target: rel.fromEntity });
} else { layoutEdges.push({ source: rel.fromEntity, target: rel.toEntity }); }
});
});
layoutEdges.forEach(e => g.setEdge(e.source, e.target));
dagre.layout(g);
g.nodes().forEach(function(v) {
const node = g.node(v);
const el = document.getElementById("table-" + v);
el.style.left = (node.x - (tableWidth / 2)) + "px";
el.style.top = (node.y - (node.height / 2)) + "px";
});
diagramData.relations.forEach(rel => {
instance.connect({
source: `field-${rel.fromEntity}-${rel.fromField}`,
target: `field-${rel.toEntity}-${rel.toField}`,
anchor: ["Continuous", { faces: ["left", "right"] }],
paintStyle: { stroke: "#7f8c8d", strokeWidth: 2 },
hoverPaintStyle: { stroke: "#3498db", strokeWidth: 3 },
overlays: [
["Label", { label: rel.fromMultiplicity || "", location: 0.1, cssClass: "relation-label" }],
["Label", { label: rel.toMultiplicity || "", location: 0.9, cssClass: "relation-label" }]
]
});
});
const tableIds = diagramData.entities.map(e => "table-" + e.id);
instance.draggable(tableIds, {containment: "parent", stop: instance.repaintEverything});
});
}
fetchInfo();
setInterval(fetchInfo, 60000);
fetchSchemaAndRender();
</script>
</body>
</html>