- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark, Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI) - aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource, an aether-core gekoppelt (Rechte über aether_users.admin_group), mit WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU) - Prototypen admin/ vervollständigt - SQL-Schema aether_admin.sql - Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
295 lines
14 KiB
HTML
295 lines
14 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="color-scheme" content="dark">
|
||
<title>Aether — Radial-Menü</title>
|
||
<style>
|
||
:root{
|
||
--fg:#f2f0fa; /* Standard-Textfarbe */
|
||
--muted:#a09bbd; /* gedämpfter Text */
|
||
--slice:rgba(18,15,30,0.82); /* Grundfarbe der Segmente */
|
||
--hub:rgba(16,13,26,0.92); /* Naben-Hintergrund */
|
||
--border:rgba(255,255,255,0.12);/* dezente Rahmen */
|
||
--accent:#7c5cf6; /* Aether-Akzent (Violett) */
|
||
--accent2:#5b8cff; /* zweiter Akzent (Blau) */
|
||
}
|
||
*{box-sizing:border-box;}
|
||
html,body{margin:0;height:100%;overflow:hidden;
|
||
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:var(--fg);}
|
||
|
||
/* ============================================================
|
||
STILISIERTE SPIELSZENE (Kulisse hinter dem Menü)
|
||
============================================================ */
|
||
.scene{position:fixed;inset:0;z-index:0;overflow:hidden;
|
||
background:linear-gradient(180deg,#141232 0%,#2a2350 34%,#5a3a66 54%,#b07a52 66%,#2a2236 66%);}
|
||
.scene .city{position:absolute;left:0;right:0;bottom:28%;height:30%;
|
||
background:linear-gradient(90deg,#201a38 0 8%,transparent 8% 13%,#2a2148 13% 21%,transparent 21% 26%,
|
||
#1c1730 26% 34%,transparent 34% 40%,#2a2148 40% 47%,transparent 47% 54%,#241d40 54% 62%,transparent 62% 68%,
|
||
#201a38 68% 77%,transparent 77% 83%,#2a2148 83% 91%,transparent 91%);}
|
||
.scene .ground{position:absolute;left:0;right:0;bottom:0;height:28%;background:linear-gradient(180deg,#161226,#0a0814);}
|
||
/* Abdunkelnde Ebene, wenn das Menü offen ist */
|
||
.dim{position:fixed;inset:0;z-index:1;background:rgba(6,5,12,0.55);backdrop-filter:blur(3px);
|
||
opacity:0;transition:.25s;pointer-events:none;}
|
||
.dim.on{opacity:1;}
|
||
|
||
/* ============================================================
|
||
RADIAL-RAD
|
||
============================================================ */
|
||
.stage{position:fixed;inset:0;z-index:2;display:grid;place-items:center;}
|
||
.wheel{position:relative;width:min(440px,88vw);aspect-ratio:1;opacity:0;transform:scale(0.9);
|
||
transition:.2s cubic-bezier(.2,.9,.3,1);pointer-events:none;}
|
||
.wheel.open{opacity:1;transform:scale(1);pointer-events:auto;}
|
||
.wheel svg{width:100%;height:100%;display:block;filter:drop-shadow(0 24px 50px rgba(0,0,0,0.6));}
|
||
|
||
/* Einzelnes Kuchen-Segment */
|
||
.slice{fill:var(--slice);stroke:rgba(0,0,0,0.35);stroke-width:1.2;cursor:pointer;transition:fill .12s;}
|
||
.slice:hover{fill:var(--c,var(--accent));} /* --c = Kategorie-Farbe */
|
||
.slice.parent:hover{fill:var(--c,var(--accent));}
|
||
.lbl-i{font-size:27px;pointer-events:none;dominant-baseline:central;text-anchor:middle;}
|
||
.lbl-t{font-size:12.5px;font-weight:700;fill:var(--fg);pointer-events:none;text-anchor:middle;
|
||
paint-order:stroke;stroke:rgba(0,0,0,0.45);stroke-width:2.4px;}
|
||
.chev{font-size:11px;fill:rgba(255,255,255,0.55);pointer-events:none;text-anchor:middle;} /* ›-Marker bei Untermenüs */
|
||
|
||
/* Nabe in der Mitte */
|
||
.hub{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:38%;aspect-ratio:1;border-radius:50%;
|
||
background:var(--hub);border:1px solid var(--border);display:grid;place-items:center;text-align:center;
|
||
backdrop-filter:blur(8px);box-shadow:inset 0 0 30px rgba(0,0,0,0.5);}
|
||
.hub.back{cursor:pointer;}
|
||
.hub .hi{font-size:32px;line-height:1;}
|
||
.hub .ht{font-size:14px;font-weight:800;margin-top:5px;padding:0 8px;}
|
||
.hub .hs{font-size:10.5px;color:var(--muted);margin-top:3px;font-weight:600;padding:0 10px;}
|
||
.hub.back .hs{color:var(--accent);}
|
||
|
||
/* Hinweis, wenn das Menü geschlossen ist */
|
||
.closed{position:fixed;inset:0;z-index:2;display:grid;place-items:center;}
|
||
.closed .card{background:var(--hub);border:1px solid var(--border);border-radius:18px;padding:26px 30px;text-align:center;
|
||
backdrop-filter:blur(10px);box-shadow:0 24px 50px -20px rgba(0,0,0,0.7);}
|
||
.closed .card .ci{font-size:40px;}
|
||
.closed .card h2{margin:10px 0 4px;font-size:20px;font-weight:800;}
|
||
.closed .card p{margin:0 0 16px;color:var(--muted);font-size:13.5px;}
|
||
.kbd{display:inline-block;background:rgba(255,255,255,0.1);border:1px solid var(--border);border-bottom-width:2px;
|
||
border-radius:7px;padding:2px 9px;font-weight:800;font-size:13px;font-family:inherit;}
|
||
.btn{border:none;border-radius:11px;padding:11px 20px;font-family:inherit;font-size:14px;font-weight:800;cursor:pointer;
|
||
background:var(--accent);color:#160c2e;transition:.12s;}
|
||
.btn:hover{filter:brightness(1.08);}
|
||
.wheel.open ~ .closed{display:none;}
|
||
|
||
/* Bestätigungs-Toast nach einer Aktion */
|
||
.toast{position:fixed;left:50%;bottom:34px;transform:translateX(-50%) translateY(16px);z-index:5;
|
||
background:var(--hub);border:1px solid var(--border);border-radius:12px;padding:12px 22px;font-size:14px;font-weight:700;
|
||
backdrop-filter:blur(10px);opacity:0;pointer-events:none;transition:.28s;box-shadow:0 16px 40px -16px rgba(0,0,0,0.7);}
|
||
.toast.show{opacity:1;transform:translateX(-50%) translateY(0);}
|
||
|
||
/* Fußzeilen-Hinweis (nur Prototyp) */
|
||
.foot{position:fixed;left:0;right:0;bottom:12px;z-index:4;text-align:center;color:var(--muted);font-size:11.5px;pointer-events:none;}
|
||
.foot .kbd{pointer-events:none;}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- Kulisse -->
|
||
<div class="scene"><div class="city"></div><div class="ground"></div></div>
|
||
<div class="dim" id="dim"></div>
|
||
|
||
<!-- Rad + geschlossener Hinweis -->
|
||
<div class="stage">
|
||
<div class="wheel" id="wheel">
|
||
<svg id="svg" viewBox="0 0 400 400"></svg>
|
||
<div class="hub" id="hub"></div>
|
||
</div>
|
||
<div class="closed" id="closed">
|
||
<div class="card">
|
||
<div class="ci">🎯</div>
|
||
<h2>Interaktionsmenü</h2>
|
||
<p>Halte <span class="kbd">G</span> gedrückt, um das Rad zu öffnen.</p>
|
||
<button class="btn" id="openBtn">Menü öffnen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toast" id="toast"></div>
|
||
<div class="foot"><span class="kbd">G</span> Menü öffnen/schließen · Klick auf ein Segment wählt · Mitte = zurück</div>
|
||
|
||
<script>
|
||
"use strict";
|
||
/* Kurzer Zugriffshelfer auf Elemente per ID */
|
||
const $=id=>document.getElementById(id);
|
||
|
||
/* =====================================================================
|
||
MENÜ-DATEN (Kategorien mit optionalen Untermenüs)
|
||
Jede Aktion ist bewusst kosmetisch — in FiveM lösen sie NUI-Callbacks aus.
|
||
===================================================================== */
|
||
const menue=[
|
||
{icon:"🙌", label:"Interaktion", farbe:"#7c5cf6", kinder:[
|
||
{icon:"🙌", label:"Hände hoch"},
|
||
{icon:"👉", label:"Zeigen"},
|
||
{icon:"🧎", label:"Knien"},
|
||
{icon:"👋", label:"Winken"},
|
||
{icon:"🪪", label:"Ausweis zeigen"}, // greift auf documents/ zurück
|
||
]},
|
||
{icon:"🚗", label:"Fahrzeug", farbe:"#5b8cff", kinder:[
|
||
{icon:"🔒", label:"Ab-/Aufschließen"},
|
||
{icon:"⚙️", label:"Motor an/aus"},
|
||
{icon:"🧳", label:"Kofferraum"},
|
||
{icon:"🚪", label:"Türen"},
|
||
{icon:"💺", label:"Sitzplatz"},
|
||
]},
|
||
{icon:"🎭", label:"Emotes", farbe:"#e06fae", kinder:[
|
||
{icon:"🪑", label:"Sitzen"},
|
||
{icon:"💃", label:"Tanzen"},
|
||
{icon:"🚬", label:"Rauchen"},
|
||
{icon:"🥤", label:"Trinken"},
|
||
{icon:"🧍", label:"Lehnen"},
|
||
{icon:"🛑", label:"Stopp / Reset"},
|
||
]},
|
||
{icon:"💼", label:"Job", farbe:"#34d39a", kinder:[
|
||
{icon:"🟢", label:"Dienst an/aus"}, // greift auf jobs/ zurück
|
||
{icon:"🧾", label:"Rechnung stellen"},
|
||
{icon:"📦", label:"Auftrag annehmen"},
|
||
{icon:"📋", label:"Job-Aktion"},
|
||
]},
|
||
{icon:"🆘", label:"Notdienst", farbe:"#f0506e", kinder:[
|
||
{icon:"🚓", label:"Polizei rufen"}, // greift auf faction/ Dispatch zurück
|
||
{icon:"🚑", label:"Rettung rufen"},
|
||
{icon:"🚨", label:"Panik / 10-13"},
|
||
]},
|
||
{icon:"🎒", label:"Inventar", farbe:"#f5a742", aktion:"Inventar geöffnet"}, // greift auf inventory/ zurück
|
||
{icon:"📱", label:"Handy", farbe:"#3fc7e0", aktion:"Handy geöffnet"}, // greift auf phone/ zurück
|
||
];
|
||
|
||
/* =====================================================================
|
||
ZUSTAND
|
||
===================================================================== */
|
||
let offen=false; // ist das Rad geöffnet?
|
||
let aktuelleKinder=null; // aktives Untermenü (null = Wurzelebene)
|
||
let aktuelleFarbe=null; // Kategorie-Farbe des aktiven Untermenüs
|
||
let elternLabel=""; // Beschriftung der übergeordneten Kategorie
|
||
|
||
/* Geometrie des Rads (im 400×400-Koordinatensystem) */
|
||
const CX=200, CY=200, R_AUSSEN=188, R_INNEN=84, GAP=2.4;
|
||
const R_ICON=(R_AUSSEN+R_INNEN)/2; // Radius für Icon/Beschriftung
|
||
|
||
/* Punkt auf einem Kreis: Winkel in Grad, von oben im Uhrzeigersinn gemessen */
|
||
function polar(r,grad){const t=grad*Math.PI/180;return [CX+r*Math.sin(t), CY-r*Math.cos(t)];}
|
||
|
||
/* Baut den SVG-Pfad eines Donut-Segments zwischen zwei Winkeln */
|
||
function segmentPfad(t0,t1){
|
||
const [x1,y1]=polar(R_AUSSEN,t0), [x2,y2]=polar(R_AUSSEN,t1);
|
||
const [x3,y3]=polar(R_INNEN,t1), [x4,y4]=polar(R_INNEN,t0);
|
||
const gross=(t1-t0)>180?1:0; // große-Bogen-Flagge
|
||
return `M${x1.toFixed(2)} ${y1.toFixed(2)} A${R_AUSSEN} ${R_AUSSEN} 0 ${gross} 1 ${x2.toFixed(2)} ${y2.toFixed(2)}`
|
||
+` L${x3.toFixed(2)} ${y3.toFixed(2)} A${R_INNEN} ${R_INNEN} 0 ${gross} 0 ${x4.toFixed(2)} ${y4.toFixed(2)} Z`;
|
||
}
|
||
|
||
/* =====================================================================
|
||
RENDERING
|
||
===================================================================== */
|
||
/* Liefert die aktuell anzuzeigende Knotenliste (Wurzel oder Untermenü) */
|
||
function aktuelleListe(){return aktuelleKinder||menue;}
|
||
|
||
/* Zeichnet das komplette Rad (Segmente + Nabe) für die aktuelle Ebene */
|
||
function zeichneRad(){
|
||
const liste=aktuelleListe();
|
||
const n=liste.length, seg=360/n;
|
||
let svg="";
|
||
liste.forEach((node,i)=>{
|
||
// Segment 0 mittig oben; Lücke (GAP) zwischen den Segmenten
|
||
const basis=i*seg-seg/2;
|
||
const t0=basis+GAP/2, t1=basis+seg-GAP/2;
|
||
const farbe=aktuelleKinder?aktuelleFarbe:node.farbe;
|
||
const [ix,iy]=polar(R_ICON,i*seg); // Mittelwinkel = i*seg
|
||
svg+=`<path class="slice ${node.kinder?'parent':''}" style="--c:${farbe}" data-i="${i}" d="${segmentPfad(t0,t1)}"></path>`;
|
||
svg+=`<text class="lbl-i" x="${ix.toFixed(1)}" y="${(iy-9).toFixed(1)}">${node.icon}</text>`;
|
||
svg+=`<text class="lbl-t" x="${ix.toFixed(1)}" y="${(iy+16).toFixed(1)}">${node.label}</text>`;
|
||
// Kleiner ›-Hinweis, dass sich ein Untermenü öffnet
|
||
if(node.kinder){const [cx,cy]=polar(R_ICON,i*seg);svg+=`<text class="chev" x="${cx.toFixed(1)}" y="${(cy+29).toFixed(1)}">▸</text>`;}
|
||
});
|
||
$("svg").innerHTML=svg;
|
||
// Segment-Ereignisse verknüpfen (Hover aktualisiert die Nabe, Klick wählt)
|
||
$("svg").querySelectorAll(".slice").forEach(p=>{
|
||
const node=liste[+p.dataset.i];
|
||
p.addEventListener("mouseenter",()=>zeigeNabe(node));
|
||
p.addEventListener("mouseleave",standardNabe);
|
||
p.addEventListener("click",()=>waehle(node));
|
||
});
|
||
standardNabe();
|
||
}
|
||
|
||
/* Zeigt in der Nabe das gerade überfahrene Segment */
|
||
function zeigeNabe(node){
|
||
$("hub").className="hub"+(aktuelleKinder?" back":"");
|
||
$("hub").innerHTML=`<div><div class="hi">${node.icon}</div><div class="ht">${node.label}</div>
|
||
<div class="hs">${node.kinder?"Untermenü öffnen ▸":(aktuelleKinder?"↩ Klick Mitte = zurück":"Auswählen")}</div></div>`;
|
||
}
|
||
/* Setzt die Nabe auf ihren Standardtext der aktuellen Ebene zurück */
|
||
function standardNabe(){
|
||
if(aktuelleKinder){
|
||
$("hub").className="hub back";
|
||
$("hub").innerHTML=`<div><div class="hi">↩</div><div class="ht">${elternLabel}</div><div class="hs">Zurück</div></div>`;
|
||
} else {
|
||
$("hub").className="hub";
|
||
$("hub").innerHTML=`<div><div class="hi">🎯</div><div class="ht">Aktionen</div><div class="hs">Kategorie wählen</div></div>`;
|
||
}
|
||
}
|
||
|
||
/* =====================================================================
|
||
NAVIGATION & AKTIONEN
|
||
===================================================================== */
|
||
/* Reaktion auf die Auswahl eines Segments */
|
||
function waehle(node){
|
||
if(node.kinder){
|
||
// In das Untermenü wechseln
|
||
aktuelleKinder=node.kinder; aktuelleFarbe=node.farbe; elternLabel=node.label;
|
||
zeichneRad();
|
||
} else {
|
||
// Blatt-Aktion ausführen, Rückmeldung geben und Rad schließen
|
||
fuehreAus(node);
|
||
}
|
||
}
|
||
/* Führt eine Blatt-Aktion aus (hier kosmetisch: Toast + schließen) */
|
||
function fuehreAus(node){
|
||
const text=node.aktion||node.label;
|
||
toast(node.icon+" "+text);
|
||
setOffen(false);
|
||
}
|
||
/* Klick auf die Nabe: aus dem Untermenü zurück zur Wurzel */
|
||
$("hub").addEventListener("click",()=>{
|
||
if(aktuelleKinder){aktuelleKinder=null;aktuelleFarbe=null;zeichneRad();}
|
||
});
|
||
|
||
/* Öffnet/schließt das Rad; beim Schließen zurück auf die Wurzelebene */
|
||
function setOffen(b){
|
||
offen=b;
|
||
$("wheel").classList.toggle("open",b);
|
||
$("dim").classList.toggle("on",b);
|
||
$("closed").style.display=b?"none":"grid";
|
||
if(b){aktuelleKinder=null;aktuelleFarbe=null;zeichneRad();}
|
||
}
|
||
|
||
/* Kurze Bestätigungs-Einblendung */
|
||
let toastTimer=null;
|
||
function toast(text){const e=$("toast");e.textContent=text;e.classList.add("show");
|
||
clearTimeout(toastTimer);toastTimer=setTimeout(()=>e.classList.remove("show"),2200);}
|
||
|
||
/* =====================================================================
|
||
STEUERUNG (Maus + Tastatur)
|
||
===================================================================== */
|
||
$("openBtn").addEventListener("click",()=>setOffen(true));
|
||
// Taste G schaltet das Rad um (verhindert Dauerfeuer beim Halten)
|
||
addEventListener("keydown",e=>{
|
||
if(e.repeat)return;
|
||
if(e.key==="g"||e.key==="G")setOffen(!offen);
|
||
if(e.key==="Escape")setOffen(false);
|
||
});
|
||
|
||
/* =====================================================================
|
||
START (geschlossen)
|
||
===================================================================== */
|
||
setOffen(false);
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|