This commit is contained in:
Team3
2026-07-26 20:53:01 +02:00
parent ebfb2fc360
commit 0115fd4ce9
13 changed files with 264 additions and 321 deletions

View File

@@ -1,7 +1,9 @@
<script setup>
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { nextTick, ref, watch } from 'vue'
import { marked } from 'marked'
import { featureSpeichern } from '../api.js'
import { oeffneMenue } from '../composables/kontextmenue.js'
import ModalBox from './ModalBox.vue'
const props = defineProps({
projekt: { type: String, required: true },
@@ -27,7 +29,7 @@ const s = ref(kopie(props.feature.sektionen))
watch(() => props.feature, f => { s.value = kopie(f.sektionen) })
const aktiv = ref(null) // {feld, i, j, original} — das eine gerade offene Eingabefeld
const gross = ref(false) // ausführliche Ansicht
const gross = ref(props.vollmodus) // ausführliche Ansicht
const entwurf = ref('')
const meldung = ref('')
const feldRef = ref(null)
@@ -60,12 +62,7 @@ async function starte(feld, i, j) {
}
function abbrechen() {
const a = aktiv.value
aktiv.value = null
// frisch angelegte, noch leere Funktion wieder entfernen
if (a && !a.original && a.feld === 'funktion') {
s.value.funktionen.splice(a.i, 1)
}
}
async function fertig() {
@@ -73,78 +70,51 @@ async function fertig() {
if (!a) return
aktiv.value = null
const neu = entwurf.value.trim()
if (neu === a.original) { abbrechenLeer(a); return }
if (neu === a.original) return
if (a.feld === 'titel') {
if (!neu) return // Titel ist Pflicht — leere Eingabe verwerfen
s.value.titel = neu
} else if (a.feld === 'funktion') {
if (!neu) return // Funktionstitel ebenfalls; entfernt wird über das Kontextmenü
s.value.funktionen[a.i].text = neu
} else if (a.feld === 'ziel') {
if (a.i === undefined) s.value.ziel = neu
else s.value.funktionen[a.i].ziel = neu
} else if (a.feld === 'funktion') {
if (!neu) s.value.funktionen.splice(a.i, 1) // leeren = löschen
else s.value.funktionen[a.i].text = neu
} else if (a.feld === 'testen') {
s.value.funktionen[a.i].testen = neu
} else if (a.feld === 'regeln') {
if (a.i === undefined) s.value.regeln = neu
else s.value.funktionen[a.i].regeln = neu
} else if (a.feld === 'testen') {
s.value.funktionen[a.i].testen = neu
}
await speichern()
}
function abbrechenLeer(a) {
if (!a.original && a.feld === 'funktion') s.value.funktionen.splice(a.i, 1)
}
// Kontextmenü: RMB auf eine Funktion (entfernen) oder auf die Karte (erstellen/Feature entfernen)
const kontextMenue = ref(null) // {funktion: index|null, x, y}
function oeffneKontextMenue(e, i = null) {
kontextMenue.value = { funktion: i, x: e.clientX, y: e.clientY }
}
function schliesseKontextMenue() {
kontextMenue.value = null
}
onMounted(() => window.addEventListener('click', schliesseKontextMenue))
onUnmounted(() => window.removeEventListener('click', schliesseKontextMenue))
// Modal für Funktion erstellen und entfernen
// Rechtsklick auf eine Funktion (entfernen) oder auf die Karte (erstellen/Feature entfernen)
const modal = ref(null) // {art: 'erstellen'} | {art: 'entfernen', i}
const modalName = ref('')
const modalFeld = ref(null)
async function menueFunktionErstellen() {
kontextMenue.value = null
modal.value = { art: 'erstellen' }
modalName.value = ''
await nextTick()
modalFeld.value?.focus()
function funktionsMenue(e, i) {
oeffneMenue(e, [
{ text: '✕ Funktion entfernen', aktion: () => (modal.value = { art: 'entfernen', i }) },
])
}
function menueFunktionEntfernen() {
const i = kontextMenue.value.funktion
kontextMenue.value = null
modal.value = { art: 'entfernen', i }
function kartenMenue(e) {
const eintraege = []
if (gross.value) {
eintraege.push({ text: '+ Funktion erstellen', aktion: () => (modal.value = { art: 'erstellen' }) })
}
eintraege.push({ text: '✕ Feature entfernen', aktion: () => emit('entfernen') })
oeffneMenue(e, eintraege)
}
function menueFeatureEntfernen() {
kontextMenue.value = null
emit('entfernen')
}
async function erstellenBestaetigen() {
const name = modalName.value.trim()
if (!name) return
s.value.funktionen.push({ text: name, ziel: '', regeln: '', testen: '' })
async function funktionErstellen(titel) {
s.value.funktionen.push({ text: titel, ziel: '', regeln: '', testen: '' })
modal.value = null
await speichern()
}
async function entfernenBestaetigen() {
async function funktionEntfernen() {
s.value.funktionen.splice(modal.value.i, 1)
modal.value = null
await speichern()
@@ -203,10 +173,6 @@ function punkte(text) {
function wechsleAnsicht() {
gross.value = !gross.value
}
onMounted(() => {
if (props.vollmodus) gross.value = true
})
</script>
<template>
@@ -215,7 +181,7 @@ onMounted(() => {
:class="[s.art, { gross }]"
@mousedown.middle.prevent
@auxclick.middle.prevent="wechsleAnsicht"
@contextmenu.prevent.stop="oeffneKontextMenue($event)"
@contextmenu.prevent.stop="kartenMenue"
>
<header>
<input
@@ -282,7 +248,7 @@ onMounted(() => {
:key="i"
class="funktion"
:class="{ offen: aktiv && aktiv.i === i }"
@contextmenu.prevent.stop="oeffneKontextMenue($event, i)"
@contextmenu.prevent.stop="funktionsMenue($event, i)"
>
<div class="zeile kopf">
<span class="pfeil"></span>
@@ -297,7 +263,7 @@ onMounted(() => {
<div v-else class="funktionstitel klickbar" @click="starte('funktion', i)">{{ f.text }}</div>
</div>
<!-- Regeln und Flow nur beim Hover über die Funktion (oder während der Bearbeitung) -->
<!-- Ziel, Regeln und Testen nur im Vollmodus -->
<div class="details">
<!-- Ziel der Funktion -->
<div v-if="istAktiv('ziel', i) || f.ziel" class="block">
@@ -357,44 +323,21 @@ onMounted(() => {
<p v-if="meldung" class="meldung">{{ meldung }}</p>
<div
v-if="kontextMenue"
class="kontextmenue"
:style="{ left: kontextMenue.x + 'px', top: kontextMenue.y + 'px' }"
>
<button v-if="kontextMenue.funktion !== null" @click="menueFunktionEntfernen"> Funktion entfernen</button>
<template v-else>
<button v-if="gross" @click="menueFunktionErstellen">+ Funktion erstellen</button>
<button @click="menueFeatureEntfernen"> Feature entfernen</button>
</template>
</div>
<div v-if="modal" class="schleier" @click.self="modal = null">
<div class="modal karte">
<template v-if="modal.art === 'erstellen'">
<h2>Funktion erstellen</h2>
<input
ref="modalFeld"
v-model="modalName"
placeholder="Funktionstitel"
@keyup.enter="erstellenBestaetigen"
@keydown.esc="modal = null"
>
<footer>
<button @click="modal = null">Abbrechen</button>
<button class="primaer" :disabled="!modalName.trim()" @click="erstellenBestaetigen">Bestätigen</button>
</footer>
</template>
<template v-else>
<h2>Funktion entfernen</h2>
<p class="frage">{{ s.funktionen[modal.i].text }} entfernen?</p>
<footer>
<button @click="modal = null">Abbrechen</button>
<button class="primaer" @click="entfernenBestaetigen">Entfernen</button>
</footer>
</template>
</div>
</div>
<ModalBox
v-if="modal && modal.art === 'erstellen'"
titel="Funktion erstellen"
platzhalter="Funktionstitel"
@bestaetigt="funktionErstellen"
@abbrechen="modal = null"
/>
<ModalBox
v-else-if="modal"
titel="Funktion entfernen"
:frage="`„${s.funktionen[modal.i].text}“ entfernen?`"
knopf="Entfernen"
@bestaetigt="funktionEntfernen"
@abbrechen="modal = null"
/>
</article>
</template>
@@ -402,8 +345,8 @@ onMounted(() => {
.karte.kern { border-color: var(--akzent); }
.karte.optional { border-color: var(--gruen); }
/* Ausführliche Ansicht: volle Breite, Funktionen in Spalten, alle Details offen.
Jede Funktion als eigene Box, Zufüge-Knöpfe nur beim Hover über die Box. */
/* Vollmodus: volle Breite, Funktionen als Boxen in responsiven Spalten,
alle Details offen */
.karte.gross { column-span: all; display: block; }
.karte.gross .liste { columns: 320px; column-gap: 1.2rem; }
.karte.gross section.leer { display: block; }
@@ -454,11 +397,6 @@ section + section { border-top: 1px solid var(--rand); }
/* Leere Sektionen nur in der ausführlichen Ansicht */
section.leer { display: none; }
.werkzeug { opacity: 0; transition: opacity 0.1s; }
.karte:hover .werkzeug, .werkzeug:focus { opacity: 1; }
/* „+ Funktion" belegt ohne Hover auch keinen Platz */
.hinzu.werkzeug { display: none; }
.karte:hover .hinzu.werkzeug { display: block; }
.hinweis {
color: var(--dezent);
@@ -484,9 +422,6 @@ section.leer { display: none; }
gap: 0.3rem;
}
.zeile .inhalt, .zeile input, .zeile textarea { flex: 1; min-width: 0; }
/* ✕ nur auf der Zeile, über der die Maus steht */
.zeile .icon-knopf { font-size: 0.8rem; opacity: 0; }
.zeile:hover .icon-knopf { opacity: 1; }
/* Funktionsliste */
.zeile.kopf { padding: 0.15rem 0.2rem; border-radius: 4px; }
@@ -494,10 +429,10 @@ section.leer { display: none; }
.funktionstitel { flex: 1; }
.pfeil { color: var(--dezent); font-size: 0.75rem; }
/* Regeln und Flow nur in der ausführlichen Ansicht */
/* Ziel, Regeln und Testen nur im Vollmodus */
.details { display: none; font-size: 0.9rem; }
/* Regeln (Punktliste) und Flow (nummerierte Schritte) als getönte Blöcke */
/* Regeln (Punktliste) und Testen (nummerierte Schritte) als getönte Blöcke */
.block {
background: var(--code-bg);
border-radius: 6px;
@@ -530,7 +465,7 @@ section.leer { display: none; }
color: var(--dezent);
font-size: 0.85em;
}
/* „+ Regeln"/„+ Flow" bündig zur Listen-Einrückung */
/* „+ Regeln"/„+ Testen" bündig zur Listen-Einrückung */
.details .hinzu {
display: block;
padding: 0.05rem 0 0.05rem 0.55rem;