WIP: device type zeichner,
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
3
init.sql
3
init.sql
@@ -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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user