WIP: device type zeichner,

This commit is contained in:
2026-02-11 15:54:11 +01:00
parent 699ffe3d6b
commit 23b687c7a2
4 changed files with 355 additions and 14 deletions

View File

@@ -22,7 +22,7 @@ if ($deviceTypeId > 0) {
[$deviceTypeId] [$deviceTypeId]
); );
if ($deviceType) { if ($deviceType) {
$ports = $sql->get( $ports = $sql->get(
"SELECT * FROM device_type_ports WHERE device_type_id = ? ORDER BY name", "SELECT * FROM device_type_ports WHERE device_type_id = ? ORDER BY name",
"i", "i",
@@ -31,6 +31,11 @@ if ($deviceTypeId > 0) {
} }
} }
$shapeDefinition = $deviceType['shape_definition'] ?? '[]';
if (trim($shapeDefinition) === '') {
$shapeDefinition = '[]';
}
$isEdit = !empty($deviceType); $isEdit = !empty($deviceType);
$pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType['name']) : "Neuer Gerätetyp"; $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType['name']) : "Neuer Gerätetyp";
@@ -104,6 +109,77 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
<?php endif; ?> <?php endif; ?>
</fieldset> </fieldset>
<fieldset>
<legend>Gerätedesign (Rechtecke, Kreise, Text)</legend>
<input type="hidden" name="shape_definition" id="shape-definition" value="<?php echo htmlspecialchars($shapeDefinition); ?>">
<div class="shape-editor">
<div class="shape-editor-canvas">
<svg id="shape-canvas" viewBox="0 0 400 200" role="img" aria-label="Gerätezeichnung">
<rect width="100%" height="100%" fill="#f8f8f8" stroke="#ddd" stroke-width="1"></rect>
</svg>
</div>
<div class="shape-editor-controls">
<div class="form-group">
<label for="shape-type">Form</label>
<select id="shape-type">
<option value="rect">Rechteck</option>
<option value="circle">Kreis</option>
<option value="text">Text</option>
</select>
</div>
<div class="shape-control-grid">
<label>
x
<input type="number" id="shape-x" value="20" step="1">
</label>
<label>
y
<input type="number" id="shape-y" value="20" step="1">
</label>
<label>
Breite
<input type="number" id="shape-width" value="120" step="1">
</label>
<label>
Höhe
<input type="number" id="shape-height" value="60" step="1">
</label>
<label>
Radius
<input type="number" id="shape-radius" value="30" step="1">
</label>
<label>
Text
<input type="text" id="shape-text" value="Label">
</label>
<label>
Füllung
<input type="color" id="shape-fill" value="#cccccc">
</label>
<label>
Strich
<input type="color" id="shape-stroke" value="#333333">
</label>
<label>
Strichbreite
<input type="number" id="shape-stroke-width" value="1" step="0.5">
</label>
</div>
<button type="button" class="button button-primary" id="shape-add">Form hinzufügen</button>
<p class="hint">Shapes werden als JSON gespeichert und können jederzeit angepasst werden.</p>
</div>
</div>
<div class="shape-list">
<h4>Shapes</h4>
<ul id="shape-list"></ul>
</div>
</fieldset>
<!-- ========================= <!-- =========================
Port-Definitionen Port-Definitionen
========================= --> ========================= -->
@@ -246,6 +322,89 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
margin-top: 30px; margin-top: 30px;
} }
.shape-editor {
display: flex;
gap: 18px;
flex-wrap: wrap;
margin-top: 20px;
}
.shape-editor-canvas {
flex: 1 1 320px;
min-width: 280px;
border: 1px solid #ddd;
border-radius: 6px;
background: white;
padding: 10px;
}
.shape-editor-canvas svg {
width: 100%;
height: 200px;
display: block;
font-family: inherit;
}
.shape-editor-controls {
flex: 1 1 220px;
min-width: 220px;
}
.shape-control-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 8px;
margin: 12px 0;
}
.shape-control-grid label {
font-size: 0.8rem;
display: flex;
flex-direction: column;
gap: 4px;
}
.shape-control-grid input,
.shape-control-grid select {
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: inherit;
}
.shape-list {
margin-top: 12px;
}
.shape-list ul {
list-style: none;
padding: 0;
margin: 6px 0 0;
border: 1px solid #eee;
border-radius: 4px;
background: #fafafa;
max-height: 130px;
overflow-y: auto;
}
.shape-list li {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid #eee;
font-size: 0.85rem;
}
.shape-list li:last-child {
border-bottom: none;
}
.shape-list button {
padding: 4px 8px;
font-size: 0.75rem;
}
.button { .button {
padding: 10px 15px; padding: 10px 15px;
background: #007bff; background: #007bff;
@@ -294,3 +453,173 @@ function confirmDelete(id) {
} }
} }
</script> </script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const hiddenInput = document.getElementById('shape-definition');
const svgCanvas = document.getElementById('shape-canvas');
const shapeList = document.getElementById('shape-list');
const addShapeButton = document.getElementById('shape-add');
const typeSelect = document.getElementById('shape-type');
const xInput = document.getElementById('shape-x');
const yInput = document.getElementById('shape-y');
const widthInput = document.getElementById('shape-width');
const heightInput = document.getElementById('shape-height');
const radiusInput = document.getElementById('shape-radius');
const textInput = document.getElementById('shape-text');
const fillInput = document.getElementById('shape-fill');
const strokeInput = document.getElementById('shape-stroke');
const strokeWidthInput = document.getElementById('shape-stroke-width');
if (!hiddenInput || !svgCanvas || !shapeList) {
return;
}
let shapes = [];
function parseShapes() {
try {
const parsed = JSON.parse(hiddenInput.value || '[]');
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
function persistShapes() {
hiddenInput.value = JSON.stringify(shapes);
}
function clearGenerated() {
svgCanvas.querySelectorAll('.generated-shape').forEach(el => el.remove());
}
function createSvgElement(name) {
return document.createElementNS('http://www.w3.org/2000/svg', name);
}
function renderCanvas() {
clearGenerated();
shapes.forEach((shape) => {
let el;
const fill = shape.fill || '#cccccc';
const stroke = shape.stroke || '#333333';
const strokeWidth = typeof shape.strokeWidth === 'number' ? shape.strokeWidth : 1;
if (shape.type === 'rect') {
el = createSvgElement('rect');
el.setAttribute('x', shape.x ?? 10);
el.setAttribute('y', shape.y ?? 10);
el.setAttribute('width', shape.width ?? 120);
el.setAttribute('height', shape.height ?? 60);
} else if (shape.type === 'circle') {
el = createSvgElement('circle');
el.setAttribute('cx', shape.x ?? 60);
el.setAttribute('cy', shape.y ?? 60);
el.setAttribute('r', shape.r ?? 30);
} else if (shape.type === 'text') {
el = createSvgElement('text');
el.setAttribute('x', shape.x ?? 30);
el.setAttribute('y', shape.y ?? 20);
el.setAttribute('fill', fill);
el.setAttribute('font-size', '18');
el.setAttribute('text-anchor', 'middle');
el.setAttribute('dominant-baseline', 'central');
el.textContent = shape.text || 'Text';
el.classList.add('generated-shape');
svgCanvas.appendChild(el);
return;
}
if (!el) {
return;
}
el.setAttribute('fill', fill);
el.setAttribute('stroke', stroke);
el.setAttribute('stroke-width', strokeWidth);
el.classList.add('generated-shape');
svgCanvas.appendChild(el);
});
}
function renderList() {
shapeList.innerHTML = '';
if (shapes.length === 0) {
const empty = document.createElement('li');
empty.innerHTML = '<em>Noch keine Formen definiert.</em>';
shapeList.appendChild(empty);
return;
}
shapes.forEach((shape, index) => {
const li = document.createElement('li');
const label = document.createElement('span');
const typeLabels = {
rect: 'Rechteck',
circle: 'Kreis',
text: 'Text'
};
const summary = `${typeLabels[shape.type] || shape.type} @ (${shape.x ?? 0}, ${shape.y ?? 0})`;
label.textContent = summary;
const removeButton = document.createElement('button');
removeButton.type = 'button';
removeButton.textContent = 'Entfernen';
removeButton.classList.add('button', 'button-small', 'button-danger');
removeButton.dataset.removeShape = index;
li.appendChild(label);
li.appendChild(removeButton);
shapeList.appendChild(li);
});
}
function getNumberValue(input, fallback) {
const value = parseFloat(input.value);
return Number.isFinite(value) ? value : fallback;
}
addShapeButton.addEventListener('click', () => {
const type = typeSelect.value;
const baseShape = {
type,
x: getNumberValue(xInput, 20),
y: getNumberValue(yInput, 20),
fill: fillInput.value || '#cccccc',
stroke: strokeInput.value || '#333333',
strokeWidth: getNumberValue(strokeWidthInput, 1)
};
if (type === 'rect') {
baseShape.width = getNumberValue(widthInput, 120);
baseShape.height = getNumberValue(heightInput, 60);
} else if (type === 'circle') {
baseShape.r = getNumberValue(radiusInput, 30);
} else if (type === 'text') {
baseShape.text = textInput.value || 'Text';
}
shapes.push(baseShape);
renderCanvas();
renderList();
persistShapes();
});
shapeList.addEventListener('click', (event) => {
if (!event.target.dataset.removeShape) {
return;
}
const index = Number(event.target.dataset.removeShape);
shapes.splice(index, 1);
renderCanvas();
renderList();
persistShapes();
});
shapes = parseShapes();
renderCanvas();
renderList();
persistShapes();
});
</script>

View File

@@ -22,6 +22,14 @@ $name = trim($_POST['name'] ?? '');
$category = $_POST['category'] ?? 'other'; $category = $_POST['category'] ?? 'other';
$comment = trim($_POST['comment'] ?? ''); $comment = trim($_POST['comment'] ?? '');
$seedPortCount = max(0, (int)($_POST['seed_ports'] ?? 0)); $seedPortCount = max(0, (int)($_POST['seed_ports'] ?? 0));
$rawShapes = trim($_POST['shape_definition'] ?? '');
$shapeDefinition = '[]';
if ($rawShapes !== '') {
$decoded = json_decode($rawShapes, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
$shapeDefinition = json_encode($decoded, JSON_UNESCAPED_UNICODE);
}
}
// ========================= // =========================
// Validierung // Validierung
@@ -88,31 +96,31 @@ if ($deviceTypeId > 0) {
// UPDATE // UPDATE
if ($imagePath) { if ($imagePath) {
$sql->set( $sql->set(
"UPDATE device_types SET name = ?, category = ?, comment = ?, image_path = ?, image_type = ? WHERE id = ?", "UPDATE device_types SET name = ?, category = ?, comment = ?, image_path = ?, image_type = ?, shape_definition = ? WHERE id = ?",
"sssisi", "ssssssi",
[$name, $category, $comment, $imagePath, $imageType, $deviceTypeId] [$name, $category, $comment, $imagePath, $imageType, $shapeDefinition, $deviceTypeId]
); );
} else { } else {
$sql->set( $sql->set(
"UPDATE device_types SET name = ?, category = ?, comment = ? WHERE id = ?", "UPDATE device_types SET name = ?, category = ?, comment = ?, shape_definition = ? WHERE id = ?",
"sssi", "ssssi",
[$name, $category, $comment, $deviceTypeId] [$name, $category, $comment, $shapeDefinition, $deviceTypeId]
); );
} }
} else { } else {
// INSERT // INSERT
if ($imagePath) { if ($imagePath) {
$deviceTypeId = $sql->set( $deviceTypeId = $sql->set(
"INSERT INTO device_types (name, category, comment, image_path, image_type) VALUES (?, ?, ?, ?, ?)", "INSERT INTO device_types (name, category, comment, image_path, image_type, shape_definition) VALUES (?, ?, ?, ?, ?, ?)",
"sssss", "ssssss",
[$name, $category, $comment, $imagePath, $imageType], [$name, $category, $comment, $imagePath, $imageType, $shapeDefinition],
true true
); );
} else { } else {
$deviceTypeId = $sql->set( $deviceTypeId = $sql->set(
"INSERT INTO device_types (name, category, comment, image_path, image_type) VALUES (?, ?, ?, NULL, ?)", "INSERT INTO device_types (name, category, comment, image_path, image_type, shape_definition) VALUES (?, ?, ?, NULL, ?, ?)",
"ssss", "sssss",
[$name, $category, $comment, 'bitmap'], [$name, $category, $comment, 'bitmap', $shapeDefinition],
true true
); );
} }

View File

@@ -119,6 +119,9 @@ Definiert eine Gerätevorlage.
- Unterstützt SVG und Bitmap (PNG/JPG) - Unterstützt SVG und Bitmap (PNG/JPG)
- Grundlage für alle grafischen Ansichten - Grundlage für alle grafischen Ansichten
**Technische Attribute**
- `shape_definition`: JSON-Array mit einfachen Formen (rect/circle/text) für die integrierte Zeichenfläche.
--- ---
### `device_type_ports` ### `device_type_ports`

View File

@@ -74,7 +74,8 @@ CREATE TABLE device_types (
category ENUM('switch','server','patchpanel','other') NOT NULL, category ENUM('switch','server','patchpanel','other') NOT NULL,
image_path VARCHAR(255), image_path VARCHAR(255),
image_type ENUM('svg','bitmap') NOT NULL, image_type ENUM('svg','bitmap') NOT NULL,
comment TEXT comment TEXT,
shape_definition JSON
) ENGINE=InnoDB; ) ENGINE=InnoDB;
CREATE TABLE device_type_ports ( CREATE TABLE device_type_ports (