Änderungen: - Anpassung der CSS-Positionen für die Haus-Ecken in der Datei TaxiToolsView.vue, um eine bessere visuelle Darstellung zu gewährleisten. - Verschiebung der Ecken um 2 Pixel nach innen, um die Platzierung zu optimieren. Diese Anpassungen verbessern die Benutzeroberfläche und die visuelle Klarheit der Hausplatzierung im Taxi-Minispiel.
1640 lines
53 KiB
Vue
1640 lines
53 KiB
Vue
<template>
|
||
<div class="contenthidden">
|
||
<div class="contentscroll">
|
||
<div class="admin-header">
|
||
<h1>{{ $t('admin.taxiTools.title') }}</h1>
|
||
<p>{{ $t('admin.taxiTools.description') }}</p>
|
||
</div>
|
||
|
||
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
||
|
||
<!-- {{ $t('admin.taxiTools.mapEditor.title') }} Tab -->
|
||
<div v-if="activeTab === 'map-editor'" class="taxi-tools-admin">
|
||
<div class="section-header">
|
||
<h2>{{ $t('admin.taxiTools.mapEditor.title') }}</h2>
|
||
</div>
|
||
|
||
<div class="map-selection">
|
||
<div class="map-count">
|
||
<p>{{ $t('admin.taxiTools.mapEditor.availableMaps', { count: maps.length }) }}</p>
|
||
</div>
|
||
|
||
<div class="map-dropdown">
|
||
<select v-model="selectedMapId" @change="onMapSelect" class="map-select">
|
||
<option value="new">{{ $t('admin.taxiTools.mapEditor.newMap') }}</option>
|
||
<option
|
||
v-for="map in maps"
|
||
:key="map.id"
|
||
:value="map.id"
|
||
class="map-option"
|
||
>
|
||
{{ map.name }}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Map Details -->
|
||
<div v-if="selectedMapId !== 'new' && selectedMap" class="map-details">
|
||
<div class="details-header">
|
||
<h3>{{ selectedMap.name }}</h3>
|
||
</div>
|
||
<div class="details-content">
|
||
<div class="form-group">
|
||
<label for="mapName">{{ $t('admin.taxiTools.mapEditor.mapName') }}:</label>
|
||
<input
|
||
id="mapName"
|
||
v-model="mapForm.name"
|
||
type="text"
|
||
required
|
||
:placeholder="$t('admin.taxiTools.mapEditor.mapName')"
|
||
>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="mapDescription">{{ $t('admin.taxiTools.mapEditor.mapDescription') }}:</label>
|
||
<textarea
|
||
id="mapDescription"
|
||
v-model="mapForm.description"
|
||
required
|
||
:placeholder="$t('admin.taxiTools.mapEditor.mapDescription')"
|
||
rows="3"
|
||
></textarea>
|
||
</div>
|
||
|
||
|
||
<div class="form-group">
|
||
<label>{{ $t('admin.taxiTools.mapEditor.mapLayout') }}:</label>
|
||
<div class="map-editor-container">
|
||
<!-- Spielbrett (links) -->
|
||
<div class="game-board">
|
||
<div class="board-grid" :style="boardGridStyle">
|
||
<div
|
||
v-for="y in range(minY, maxY)"
|
||
:key="`row-${y}`"
|
||
class="board-row"
|
||
>
|
||
<div
|
||
v-for="x in range(minX, maxX)"
|
||
:key="`${x},${y}`"
|
||
class="board-cell"
|
||
:class="getCellClasses(getCellAtPosition(x, y), `${x},${y}`)"
|
||
@click="selectCell(`${x},${y}`)"
|
||
:title="`Position: (${x}, ${y})`"
|
||
>
|
||
<img
|
||
v-if="getCellAtPosition(x, y)?.tileType"
|
||
:src="`/images/taxi/map-${getCellAtPosition(x, y).tileType}.svg`"
|
||
:alt="getCellAtPosition(x, y).tileType"
|
||
class="tile-image"
|
||
/>
|
||
<div v-if="getCellAtPosition(x, y)?.extraHouses" class="house-overlay">
|
||
<div
|
||
v-for="(rotation, corner) in getCellAtPosition(x, y).extraHouses"
|
||
:key="corner"
|
||
class="house-square"
|
||
:class="cornerClass(corner)"
|
||
>
|
||
<div class="house-door" :class="doorClass(rotation)"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tile-Palette (rechts) -->
|
||
<div class="tile-palette tool-panel">
|
||
<h4>{{ $t('admin.taxiTools.mapEditor.tilePalette') }}</h4>
|
||
<div class="tile-grid">
|
||
<div
|
||
v-for="tileType in availableTileTypes"
|
||
:key="tileType"
|
||
class="tile-option"
|
||
:class="{ 'selected': selectedTileType === tileType }"
|
||
@click="selectTileType(tileType)"
|
||
:title="tileType"
|
||
>
|
||
<img
|
||
:src="`/images/taxi/map-${tileType}.svg`"
|
||
:alt="tileType"
|
||
class="tile-image"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Straßennamen (drittes Panel) -->
|
||
<div v-if="showStreetNamePanel" class="tool-panel street-names">
|
||
<h4>{{ $t('admin.taxiTools.mapEditor.streetNames') }}</h4>
|
||
<div class="panel-body">
|
||
<div class="form-group-mini" v-if="supportsHorizontalName">
|
||
<label class="arrow-label">↔</label>
|
||
<input type="text" :value="selectedCell?.streetNameH || ''"
|
||
@input="onChangeStreetName('h', $event.target.value)" />
|
||
</div>
|
||
<div class="form-group-mini" v-if="supportsVerticalName">
|
||
<label class="arrow-label">↕</label>
|
||
<input type="text" :value="selectedCell?.streetNameV || ''"
|
||
@input="onChangeStreetName('v', $event.target.value)" />
|
||
</div>
|
||
<div class="form-actions-mini" v-if="showContinueOtherButton">
|
||
<button type="button" class="btn btn-primary mini" @click="continueStreetNames">
|
||
{{ $t('admin.taxiTools.mapEditor.continueOther') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Zusätzliche Elemente (viertes Panel) -->
|
||
<div v-if="showExtraElementsPanel" class="tool-panel extra-elements">
|
||
<h4>{{ $t('admin.taxiTools.mapEditor.extraElements') }}</h4>
|
||
<div class="panel-body">
|
||
<div v-if="allowedHouseCorners.length" class="corner-chooser">
|
||
<button v-for="pos in allowedHouseCorners" :key="pos" class="corner-btn" @click="confirmHouseCorner(pos)">
|
||
{{ pos.toUpperCase() }}
|
||
</button>
|
||
</div>
|
||
<h5 v-if="pendingCorner" class="extra-subtitle">Haus</h5>
|
||
<div v-if="pendingCorner" class="extra-grid">
|
||
<div
|
||
v-for="dir in availableHouseDirections"
|
||
:key="dir"
|
||
class="extra-option"
|
||
@click="startPlaceHouse(directionToRotation(dir))"
|
||
:title="`Haus: Tür ${dir}`">
|
||
<div class="house-square">
|
||
<div class="house-door" :class="doorCssClass(dir)"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-danger" @click="deleteSelectedMap">
|
||
{{ $t('admin.taxiTools.mapEditor.delete') }}
|
||
</button>
|
||
<button type="button" class="btn btn-primary" @click="saveMap">
|
||
{{ $t('admin.taxiTools.mapEditor.update') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Map Form (nur für neue Maps) -->
|
||
<div v-if="selectedMapId === 'new'" class="map-form">
|
||
<div class="form-header">
|
||
<h3>{{ $t('admin.taxiTools.mapEditor.newMap') }}</h3>
|
||
</div>
|
||
|
||
<form @submit.prevent="saveMap">
|
||
<div class="form-group">
|
||
<label for="mapName">{{ $t('admin.taxiTools.mapEditor.mapName') }}:</label>
|
||
<input
|
||
id="mapName"
|
||
v-model="mapForm.name"
|
||
type="text"
|
||
required
|
||
:placeholder="$t('admin.taxiTools.mapEditor.mapName')"
|
||
>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="mapDescription">{{ $t('admin.taxiTools.mapEditor.mapDescription') }}:</label>
|
||
<textarea
|
||
id="mapDescription"
|
||
v-model="mapForm.description"
|
||
required
|
||
:placeholder="$t('admin.taxiTools.mapEditor.mapDescription')"
|
||
rows="3"
|
||
></textarea>
|
||
</div>
|
||
|
||
|
||
<div class="form-group">
|
||
<label>{{ $t('admin.taxiTools.mapEditor.mapLayout') }}:</label>
|
||
<div class="map-editor-container">
|
||
<!-- Spielbrett (links) -->
|
||
<div class="game-board">
|
||
<div class="board-grid" :style="boardGridStyle">
|
||
<div
|
||
v-for="y in range(minY, maxY)"
|
||
:key="`row-${y}`"
|
||
class="board-row"
|
||
>
|
||
<div
|
||
v-for="x in range(minX, maxX)"
|
||
:key="`${x},${y}`"
|
||
class="board-cell"
|
||
:class="getCellClasses(getCellAtPosition(x, y), `${x},${y}`)"
|
||
@click="selectCell(`${x},${y}`)"
|
||
:title="`Position: (${x}, ${y})`"
|
||
>
|
||
<img
|
||
v-if="getCellAtPosition(x, y)?.tileType"
|
||
:src="`/images/taxi/map-${getCellAtPosition(x, y).tileType}.svg`"
|
||
:alt="getCellAtPosition(x, y).tileType"
|
||
class="tile-image"
|
||
/>
|
||
<div v-if="getCellAtPosition(x, y)?.extraHouses" class="house-overlay">
|
||
<div
|
||
v-for="(rotation, corner) in getCellAtPosition(x, y).extraHouses"
|
||
:key="corner"
|
||
class="house-square"
|
||
:class="cornerClass(corner)"
|
||
>
|
||
<div class="house-door" :class="doorClass(rotation)"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tile-Palette (rechts) -->
|
||
<div class="tile-palette tool-panel">
|
||
<h4>{{ $t('admin.taxiTools.mapEditor.tilePalette') }}</h4>
|
||
<div class="tile-grid">
|
||
<div
|
||
v-for="tileType in availableTileTypes"
|
||
:key="tileType"
|
||
class="tile-option"
|
||
:class="{ 'selected': selectedTileType === tileType }"
|
||
@click="selectTileType(tileType)"
|
||
:title="tileType"
|
||
>
|
||
<img
|
||
:src="`/images/taxi/map-${tileType}.svg`"
|
||
:alt="tileType"
|
||
class="tile-image"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Straßennamen (drittes Panel) -->
|
||
<div v-if="showStreetNamePanel" class="tool-panel street-names">
|
||
<h4>{{ $t('admin.taxiTools.mapEditor.streetNames') }}</h4>
|
||
<div class="panel-body">
|
||
<div class="form-group-mini" v-if="supportsHorizontalName">
|
||
<label class="arrow-label">↔</label>
|
||
<input type="text" :value="selectedCell?.streetNameH || ''"
|
||
@input="onChangeStreetName('h', $event.target.value)" />
|
||
</div>
|
||
<div class="form-group-mini" v-if="supportsVerticalName">
|
||
<label class="arrow-label">↕</label>
|
||
<input type="text" :value="selectedCell?.streetNameV || ''"
|
||
@input="onChangeStreetName('v', $event.target.value)" />
|
||
</div>
|
||
<div class="form-actions-mini" v-if="showContinueOtherButton">
|
||
<button type="button" class="btn btn-primary mini" @click="continueStreetNames">
|
||
{{ $t('admin.taxiTools.mapEditor.continueOther') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Zusätzliche Elemente (viertes Panel) -->
|
||
<div v-if="showExtraElementsPanel" class="tool-panel extra-elements">
|
||
<h4>{{ $t('admin.taxiTools.mapEditor.extraElements') }}</h4>
|
||
<div class="panel-body">
|
||
<div v-if="allowedHouseCorners.length" class="corner-chooser">
|
||
<button
|
||
v-for="pos in allowedHouseCorners"
|
||
:key="pos"
|
||
class="corner-btn"
|
||
:title="pos.toUpperCase()"
|
||
@click="confirmHouseCorner(pos)">
|
||
<div class="corner-preview">
|
||
<span class="q" :class="{ active: pos === 'lo' }"></span>
|
||
<span class="q" :class="{ active: pos === 'ro' }"></span>
|
||
<span class="q" :class="{ active: pos === 'lu' }"></span>
|
||
<span class="q" :class="{ active: pos === 'ru' }"></span>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
<h5 v-if="pendingCorner" class="extra-subtitle">Haus</h5>
|
||
<div v-if="pendingCorner" class="extra-grid">
|
||
<div
|
||
v-for="dir in availableHouseDirections"
|
||
:key="dir"
|
||
class="extra-option"
|
||
@click="startPlaceHouse(directionToRotation(dir))"
|
||
:title="`Haus: Tür ${dir}`">
|
||
<div class="house-square">
|
||
<div class="house-door" :class="doorCssClass(dir)"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-secondary" @click="cancelEdit">
|
||
{{ $t('admin.taxiTools.mapEditor.cancel') }}
|
||
</button>
|
||
<button type="submit" class="btn btn-primary">
|
||
{{ $t('admin.taxiTools.mapEditor.create') }}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Message Dialog -->
|
||
<MessageDialog ref="messageDialog" />
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import SimpleTabs from '../../components/SimpleTabs.vue';
|
||
import MessageDialog from '../../dialogues/standard/MessageDialog.vue';
|
||
import apiClient from '../../utils/axios.js';
|
||
|
||
// Matrix: erlaubte Haus-Tür-Richtungen je Tile-Typ und Ecke
|
||
// Richtungen: bottom, right, top, left
|
||
const HOUSE_TYPE_MATRIX = {
|
||
horizontal: { lo: ['bottom'], ro: ['bottom'], lu: ['top'], ru: ['top'] },
|
||
vertical: { lo: ['right'], ro: ['left'], lu: ['right'], ru: ['left'] },
|
||
cross: { lo: ['bottom', 'right'], ro: ['bottom', 'left'], lu: ['top', 'right'], ru: ['top', 'left'] },
|
||
cornertopleft: { lo: ['bottom', 'right'], ro: ['left'], lu: ['top'], ru: [] },
|
||
cornertopright: { lo: ['right'], ro: ['bottom', 'right'], lu: [], ru: ['top'] },
|
||
cornerbottomleft: { lo: ['bottom'], ro: [], lu: ['top', 'right'], ru: ['left'] },
|
||
cornerbottomright: { lo: [], ro: ['bottom'], lu: ['right'], ru: ['top', 'left'] },
|
||
tup: { lo: ['bottom', 'right'], ro: ['bottom', 'left'], lu: ['top'], ru: ['top'] },
|
||
tdown: { lo: ['bottom'], ro: ['bottom'], lu: ['top', 'right'], ru: ['top', 'left'] },
|
||
tleft: { lo: ['bottom', 'right'], ro: ['left'], lu: ['top', 'right'], ru: ['left'] },
|
||
tright: { lo: ['right'], ro: ['bottom', 'left'], lu: ['right'], ru: ['top', 'left'] },
|
||
fuelhorizontal: { lo: [], ro: [], lu: ['top'], ru: ['top'] },
|
||
fuelvertical: { lo: ['right'], ro: [], lu: ['right'], ru: [] }
|
||
};
|
||
|
||
const DIRECTION_TO_ROTATION = { bottom: 0, right: 90, top: 180, left: 270 };
|
||
|
||
export default {
|
||
name: 'AdminTaxiToolsView',
|
||
components: {
|
||
SimpleTabs,
|
||
MessageDialog
|
||
},
|
||
data() {
|
||
return {
|
||
activeTab: 'map-editor',
|
||
tabs: [
|
||
{
|
||
value: 'map-editor',
|
||
label: this.$t('admin.taxiTools.mapEditor.title')
|
||
}
|
||
],
|
||
maps: [],
|
||
selectedMapId: 'new',
|
||
editingMap: null,
|
||
mapForm: {
|
||
name: '',
|
||
description: '',
|
||
// mapData entfernt – Tiles werden separat gesendet
|
||
},
|
||
boardCells: {}, // { "x,y": { tileType: "horizontal", x: 0, y: 0 } }
|
||
availableTileTypes: [
|
||
// 1. Zeile: bottomright, tdown, horizontal, bottomleft
|
||
'cornerbottomright', 'tdown', 'horizontal', 'cornerbottomleft',
|
||
// 2. Zeile: topright, tup, fuelhorizontal, topleft
|
||
'cornertopright', 'tup', 'fuelhorizontal', 'cornertopleft',
|
||
// 3. Zeile: tright, tleft, vertical, fuelvertical
|
||
'tright', 'tleft', 'vertical', 'fuelvertical',
|
||
// 4. Zeile: cross
|
||
'cross'
|
||
],
|
||
selectedCellKey: null,
|
||
selectedTileType: null
|
||
,pendingHouse: null
|
||
,allowedHouseCorners: []
|
||
,pendingCorner: null
|
||
};
|
||
},
|
||
computed: {
|
||
selectedMap() {
|
||
if (this.selectedMapId === 'new') return null;
|
||
return this.maps.find(m => m.id === this.selectedMapId);
|
||
},
|
||
|
||
boardWidth() {
|
||
if (Object.keys(this.boardCells).length === 0) return 0;
|
||
return this.maxX - this.minX + 1;
|
||
},
|
||
|
||
boardHeight() {
|
||
if (Object.keys(this.boardCells).length === 0) return 0;
|
||
return this.maxY - this.minY + 1;
|
||
},
|
||
selectedCell() {
|
||
if (!this.selectedCellKey) return null;
|
||
return this.boardCells[this.selectedCellKey] || null;
|
||
},
|
||
showStreetNamePanel() {
|
||
// Panel nur anzeigen, wenn Zelle selektiert ist und der Tile-Typ Namen unterstützt
|
||
if (!this.selectedCell) return false;
|
||
const type = this.selectedCell.tileType;
|
||
return this.supportsHorizontal(type) || this.supportsVertical(type);
|
||
},
|
||
showExtraElementsPanel() {
|
||
// Nur sichtbar, wenn eine Zelle selektiert ist UND ein Tile-Typ gesetzt ist
|
||
return !!(this.selectedCell && this.selectedCell.tileType);
|
||
},
|
||
supportsHorizontalName() {
|
||
if (!this.selectedCell) return false;
|
||
return this.supportsHorizontal(this.selectedCell.tileType);
|
||
},
|
||
supportsVerticalName() {
|
||
if (!this.selectedCell) return false;
|
||
return this.supportsVertical(this.selectedCell.tileType);
|
||
},
|
||
showContinueOtherButton() {
|
||
if (!this.selectedCell) return false;
|
||
const hAllowed = this.supportsHorizontalName;
|
||
const vAllowed = this.supportsVerticalName;
|
||
if (!(hAllowed && vAllowed)) return false;
|
||
const hasH = !!this.selectedCell.streetNameH;
|
||
const hasV = !!this.selectedCell.streetNameV;
|
||
// genau einer gesetzt
|
||
return (hasH && !hasV) || (!hasH && hasV);
|
||
},
|
||
|
||
minX() {
|
||
if (Object.keys(this.boardCells).length === 0) return 0;
|
||
return Math.min(...Object.values(this.boardCells).map(cell => cell.x));
|
||
},
|
||
|
||
maxX() {
|
||
if (Object.keys(this.boardCells).length === 0) return 0;
|
||
return Math.max(...Object.values(this.boardCells).map(cell => cell.x));
|
||
},
|
||
|
||
minY() {
|
||
if (Object.keys(this.boardCells).length === 0) return 0;
|
||
return Math.min(...Object.values(this.boardCells).map(cell => cell.y));
|
||
},
|
||
|
||
maxY() {
|
||
if (Object.keys(this.boardCells).length === 0) return 0;
|
||
return Math.max(...Object.values(this.boardCells).map(cell => cell.y));
|
||
},
|
||
|
||
boardGridStyle() {
|
||
if (this.boardWidth === 0 || this.boardHeight === 0) {
|
||
return {
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
minHeight: '200px',
|
||
border: '2px dashed #ccc',
|
||
background: '#f9f9f9'
|
||
};
|
||
}
|
||
|
||
return {
|
||
display: 'grid',
|
||
gridTemplateColumns: `repeat(${this.boardWidth}, 50px)`,
|
||
gridTemplateRows: `repeat(${this.boardHeight}, 50px)`,
|
||
gap: '2px'
|
||
};
|
||
},
|
||
|
||
// Verfügbare Haus-Tür-Richtungen anhand ausgewählter Ecke und Tile-Typ
|
||
availableHouseDirections() {
|
||
if (!this.pendingCorner || !this.selectedCell || !this.selectedCell.tileType) return [];
|
||
const entry = HOUSE_TYPE_MATRIX[this.selectedCell.tileType];
|
||
if (!entry) return [];
|
||
const dirs = entry[this.pendingCorner] || [];
|
||
return Array.isArray(dirs) ? dirs : [];
|
||
}
|
||
},
|
||
|
||
watch: {
|
||
selectedMapId: {
|
||
handler(newValue, oldValue) {
|
||
if (newValue !== oldValue) {
|
||
this.$nextTick(() => {
|
||
this.onMapSelect();
|
||
});
|
||
}
|
||
},
|
||
immediate: true
|
||
}
|
||
},
|
||
mounted() {
|
||
this.maps = [];
|
||
this.editingMap = null;
|
||
this.boardCells = {};
|
||
|
||
this.$nextTick(() => {
|
||
this.loadMaps();
|
||
this.initializeBoard();
|
||
});
|
||
},
|
||
|
||
methods: {
|
||
directionToRotation(dir) {
|
||
return DIRECTION_TO_ROTATION[dir] ?? 0;
|
||
},
|
||
doorCssClass(dir) {
|
||
switch (dir) {
|
||
case 'bottom': return 'door-bottom';
|
||
case 'right': return 'door-right';
|
||
case 'top': return 'door-top';
|
||
case 'left': return 'door-left';
|
||
default: return 'door-bottom';
|
||
}
|
||
},
|
||
supportsHorizontal(tileType) {
|
||
// Horizontalen Straßennamen erlauben bei: horizontal, cross, tleft, tright, tup, tdown, corns wo horizontaler Ast existiert
|
||
const allowed = ['horizontal', 'cross', 'tleft', 'tright', 'tup', 'tdown', 'cornertopleft', 'cornerbottomleft', 'cornertopright', 'cornerbottomright', 'fuelhorizontal'];
|
||
return allowed.includes(tileType);
|
||
},
|
||
supportsVertical(tileType) {
|
||
// Vertikalen Straßennamen erlauben bei: vertical, cross, tup, tdown, etc.
|
||
const allowed = ['vertical', 'cross', 'tleft', 'tright', 'tup', 'tdown', 'cornertopleft', 'cornerbottomleft', 'cornertopright', 'cornerbottomright', 'fuelvertical'];
|
||
return allowed.includes(tileType);
|
||
},
|
||
continueStreetNames() {
|
||
if (!this.selectedCell) return;
|
||
// Wenn nur einer der beiden Namen existiert, kopiere in das andere Feld
|
||
const hasH = !!this.selectedCell.streetNameH;
|
||
const hasV = !!this.selectedCell.streetNameV;
|
||
if (hasH && !hasV) {
|
||
const value = this.selectedCell.streetNameH;
|
||
this.onChangeStreetName('v', value);
|
||
} else if (!hasH && hasV) {
|
||
const value = this.selectedCell.streetNameV;
|
||
this.onChangeStreetName('h', value);
|
||
}
|
||
},
|
||
onChangeStreetName(dir, value) {
|
||
if (!this.selectedCell) return;
|
||
const key = dir === 'h' ? 'streetNameH' : 'streetNameV';
|
||
// Vue reaktiv: bestehendes Objekt erweitern
|
||
const updated = { ...(this.boardCells[this.selectedCellKey] || {}), [key]: value };
|
||
this.$set ? this.$set(this.boardCells, this.selectedCellKey, updated) : (this.boardCells[this.selectedCellKey] = updated);
|
||
// Namen entlang der Achse bis zu Abschluss-Tiles fortführen
|
||
this.propagateStreetNameChain(this.selectedCellKey, dir, value);
|
||
},
|
||
continueStreetName(dir) {
|
||
if (!this.selectedCell) return;
|
||
const name = dir === 'h' ? (this.selectedCell.streetNameH || '') : (this.selectedCell.streetNameV || '');
|
||
if (!name) return;
|
||
this.propagateStreetNameChain(this.selectedCellKey, dir, name);
|
||
},
|
||
|
||
// Führt Namen entlang der Achse fort, bis ein Abschluss-Tile erreicht ist
|
||
propagateStreetNameChain(startKey, dir, name) {
|
||
const start = this.boardCells[startKey];
|
||
if (!start || !start.tileType) return;
|
||
const { x, y } = start;
|
||
const keyField = dir === 'h' ? 'streetNameH' : 'streetNameV';
|
||
|
||
const walk = (sx, sy, sign) => {
|
||
let cx = sx;
|
||
let cy = sy;
|
||
while (true) {
|
||
if (dir === 'h') cx += (sign === 'right' ? 1 : -1); else cy += (sign === 'down' ? 1 : -1);
|
||
const key = `${cx},${cy}`;
|
||
const cell = this.boardCells[key];
|
||
if (!cell || !cell.tileType) break;
|
||
const status = this.classifyNeighbor(dir, sign, cell.tileType);
|
||
if (status === 'none') break;
|
||
const upd = { ...cell, [keyField]: name };
|
||
this.$set ? this.$set(this.boardCells, key, upd) : (this.boardCells[key] = upd);
|
||
if (status === 'terminal') break;
|
||
}
|
||
};
|
||
|
||
const dirs = this.allowedDirections(start.tileType);
|
||
if (dir === 'h') {
|
||
if (dirs.right) walk(x, y, 'right');
|
||
if (dirs.left) walk(x, y, 'left');
|
||
} else {
|
||
if (dirs.down) walk(x, y, 'down');
|
||
if (dirs.up) walk(x, y, 'up');
|
||
}
|
||
},
|
||
|
||
classifyNeighbor(dir, sign, type) {
|
||
const isCornerRight = (t) => /corner.*right$/.test(t);
|
||
const isCornerLeft = (t) => /corner.*left$/.test(t);
|
||
const isCornerTop = (t) => /cornertop.*$/.test(t);
|
||
const isCornerBottom= (t) => /cornerbottom.*$/.test(t);
|
||
if (dir === 'h') {
|
||
// horizontal folgen können: corner<xxx>right, horizontal, tdown, tup, cross, fuelhorizontal, sowie corner<xxx>left und tleft als Abschluss
|
||
if (sign === 'right') {
|
||
if (isCornerLeft(type) || type === 'tleft') return 'terminal';
|
||
if (isCornerRight(type) || ['horizontal','tdown','tup','cross','fuelhorizontal'].includes(type)) return 'continue';
|
||
return 'none';
|
||
} else { // left
|
||
if (isCornerRight(type) || type === 'tright') return 'terminal';
|
||
if (isCornerLeft(type) || ['horizontal','tdown','tup','cross','fuelhorizontal'].includes(type)) return 'continue';
|
||
return 'none';
|
||
}
|
||
} else {
|
||
// vertikal können folgen: cornerbottom<xxx>, vertical, tleft, tright, cross, fuelvertical, cornerup<xxx>
|
||
// Abschluss bei Richtung down: cornerup<xxx> und tup (wir setzen noch, führen aber nicht weiter)
|
||
if (sign === 'down') {
|
||
if (isCornerTop(type) || type === 'tup') return 'terminal';
|
||
if (isCornerBottom(type) || ['vertical','tleft','tright','cross','fuelvertical'].includes(type)) return 'continue';
|
||
return 'none';
|
||
} else { // up
|
||
if (isCornerBottom(type) || type === 'tdown') return 'terminal';
|
||
if (isCornerTop(type) || ['vertical','tleft','tright','cross','fuelvertical'].includes(type)) return 'continue';
|
||
return 'none';
|
||
}
|
||
}
|
||
},
|
||
|
||
allowedDirections(tileType) {
|
||
// Gibt zurück, in welche Richtungen dieses Tile Anschlüsse hat
|
||
switch (tileType) {
|
||
case 'cornertopleft': return { left: true, up: true, right: false, down: false };
|
||
case 'cornertopright': return { right: true, up: true, left: false, down: false };
|
||
case 'cornerbottomleft': return { left: true, down: true, right: false, up: false };
|
||
case 'cornerbottomright': return { right: true, down: true, left: false, up: false };
|
||
case 'horizontal':
|
||
case 'fuelhorizontal': return { left: true, right: true, up: false, down: false };
|
||
case 'vertical':
|
||
case 'fuelvertical': return { up: true, down: true, left: false, right: false };
|
||
case 'cross': return { left: true, right: true, up: true, down: true };
|
||
case 'tup': return { up: true, left: true, right: true, down: false };
|
||
case 'tdown': return { down: true, left: true, right: true, up: false };
|
||
case 'tleft': return { left: true, up: true, down: true, right: false };
|
||
case 'tright': return { right: true, up: true, down: true, left: false };
|
||
default: return { left: false, right: false, up: false, down: false };
|
||
}
|
||
},
|
||
|
||
propagateStreetNameFrom(startKey, dir, name) {
|
||
const keyField = dir === 'h' ? 'streetNameH' : 'streetNameV';
|
||
const step = dir === 'h' ? { dx: 1, dy: 0 } : { dx: 0, dy: 1 };
|
||
const oppositeStep = { dx: -step.dx, dy: -step.dy };
|
||
// in positive Richtung fortführen
|
||
this.walkAndSet(startKey, step, dir, keyField, name);
|
||
// in negative Richtung fortführen
|
||
this.walkAndSet(startKey, oppositeStep, dir, keyField, name);
|
||
},
|
||
|
||
walkAndSet(startKey, step, dir, keyField, name) {
|
||
const canContinue = (tileType, dir) => {
|
||
switch (tileType) {
|
||
case 'horizontal': return dir === 'h';
|
||
case 'vertical': return dir === 'v';
|
||
case 'cross': return true;
|
||
case 'fuelhorizontal': return dir === 'h';
|
||
case 'fuelvertical': return dir === 'v';
|
||
case 'cornertopleft':
|
||
case 'cornertopright':
|
||
case 'cornerbottomleft':
|
||
case 'cornerbottomright':
|
||
return false; // Kurve blockt die Fortführung
|
||
case 'tup': return dir === 'h';
|
||
case 'tdown': return dir === 'h';
|
||
case 'tleft': return dir === 'v';
|
||
case 'tright': return dir === 'v';
|
||
default: return false;
|
||
}
|
||
};
|
||
let [x, y] = startKey.split(',').map(Number);
|
||
while (true) {
|
||
x += step.dx; y += step.dy;
|
||
const key = `${x},${y}`;
|
||
const cell = this.boardCells[key];
|
||
if (!cell || !cell.tileType) break;
|
||
if (!canContinue(cell.tileType, dir)) break;
|
||
const updated = { ...cell, [keyField]: name };
|
||
this.$set ? this.$set(this.boardCells, key, updated) : (this.boardCells[key] = updated);
|
||
}
|
||
},
|
||
async loadMaps() {
|
||
try {
|
||
const user = this.$store.getters.user;
|
||
if (!user || !user.authCode) {
|
||
setTimeout(() => this.loadMaps(), 100);
|
||
return;
|
||
}
|
||
|
||
const response = await apiClient.get('/api/taxi-maps/maps');
|
||
this.maps = response.data.data || [];
|
||
} catch (error) {
|
||
console.error('Fehler beim Laden der Maps:', error);
|
||
}
|
||
},
|
||
|
||
|
||
async onMapSelect() {
|
||
if (this.selectedMapId === 'new') {
|
||
this.createMap();
|
||
} else {
|
||
const map = this.selectedMap;
|
||
|
||
if (map) {
|
||
await this.editMap(map);
|
||
} else {
|
||
console.warn('Keine Map gefunden für ID:', this.selectedMapId);
|
||
}
|
||
}
|
||
},
|
||
|
||
createMap() {
|
||
this.selectedMapId = 'new';
|
||
this.editingMap = null;
|
||
this.selectedCellKey = null;
|
||
this.selectedTileType = null;
|
||
this.mapForm = {
|
||
name: '',
|
||
description: '',
|
||
mapData: []
|
||
};
|
||
this.initializeBoard();
|
||
},
|
||
|
||
async editMap(map) {
|
||
this.editingMap = map;
|
||
this.selectedCellKey = null;
|
||
this.selectedTileType = null;
|
||
this.mapForm = {
|
||
name: map.name,
|
||
description: map.description,
|
||
};
|
||
// Falls Tiles aus Backend mitkommen, Board daraus erstellen
|
||
if (Array.isArray(map.tiles) && map.tiles.length > 0) {
|
||
this.boardCells = {};
|
||
for (const t of map.tiles) {
|
||
const key = `${t.x},${t.y}`;
|
||
this.boardCells[key] = { tileType: t.tileType, x: t.x, y: t.y };
|
||
if (t.meta && t.meta.house) {
|
||
// Legacy Einzel-Haus zu neuer Struktur migrieren
|
||
const corner = t.meta.house.corner || 'lo';
|
||
const rotation = t.meta.house.rotation || 0;
|
||
this.boardCells[key].extraHouses = { [corner]: rotation };
|
||
}
|
||
if (t.meta && t.meta.houses) {
|
||
// Neue Struktur: Mapping corner -> rotation
|
||
this.boardCells[key].extraHouses = { ...t.meta.houses };
|
||
}
|
||
}
|
||
// Straßennamen aus tileStreets übernehmen
|
||
if (Array.isArray(map.tileStreets)) {
|
||
for (const s of map.tileStreets) {
|
||
const key = `${s.x},${s.y}`;
|
||
if (this.boardCells[key]) {
|
||
this.boardCells[key] = {
|
||
...this.boardCells[key],
|
||
streetNameH: s.streetNameH?.name || null,
|
||
streetNameV: s.streetNameV?.name || null
|
||
};
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
this.initializeBoard();
|
||
}
|
||
},
|
||
|
||
async deleteSelectedMap() {
|
||
if (this.selectedMapId && this.selectedMapId !== 'new') {
|
||
await this.deleteMap(this.selectedMapId);
|
||
this.selectedMapId = 'new';
|
||
}
|
||
},
|
||
|
||
selectCell(key) {
|
||
// Wähle Position aus
|
||
this.selectedCellKey = this.selectedCellKey === key ? null : key;
|
||
// Reset temporärer Zustände
|
||
this.selectedTileType = null;
|
||
this.pendingHouse = null;
|
||
this.pendingCorner = null;
|
||
// Erlaubte Ecken vorberechnen
|
||
this.computeAllowedHouseCorners();
|
||
},
|
||
|
||
selectTileType(tileType) {
|
||
// Wähle Tile-Typ aus
|
||
this.selectedTileType = this.selectedTileType === tileType ? null : tileType;
|
||
|
||
// Wenn eine Position ausgewählt ist, platziere das Tile
|
||
if (this.selectedCellKey !== null) {
|
||
this.placeTile(this.selectedCellKey, tileType);
|
||
// Nach dem Setzen bleibt die Zelle ausgewählt
|
||
// Tile-Typ-Auswahl bleibt erhalten, um mehrere gleiche Tiles zu setzen
|
||
}
|
||
},
|
||
|
||
getCellPosition(cellKey) {
|
||
const [x, y] = cellKey.split(',').map(Number);
|
||
return { x, y };
|
||
},
|
||
|
||
placeTile(cellKey, tileType) {
|
||
const [x, y] = cellKey.split(',').map(Number);
|
||
|
||
this.boardCells[cellKey] = {
|
||
tileType: tileType,
|
||
x: x,
|
||
y: y
|
||
};
|
||
|
||
// Nach dem Setzen: Zelle ausgewählt lassen
|
||
this.selectedCellKey = cellKey;
|
||
|
||
// Erweitere das Board um benachbarte leere Zellen falls nötig
|
||
this.expandBoardIfNeeded(x, y, tileType);
|
||
// Erlaubte Haus-Ecken aktualisieren
|
||
this.computeAllowedHouseCorners();
|
||
},
|
||
|
||
startPlaceHouse(rotationDeg) {
|
||
// Neuer Flow: Rotation wählen, nachdem Ecke gewählt wurde
|
||
if (!this.selectedCellKey || !this.pendingCorner) return;
|
||
const cell = this.boardCells[this.selectedCellKey];
|
||
if (!cell || !cell.tileType) return;
|
||
const existing = cell.extraHouses || {};
|
||
const next = { ...existing, [this.pendingCorner]: rotationDeg };
|
||
const updated = { ...cell, extraHouses: next };
|
||
this.$set ? this.$set(this.boardCells, this.selectedCellKey, updated) : (this.boardCells[this.selectedCellKey] = updated);
|
||
this.pendingCorner = null;
|
||
},
|
||
confirmHouseCorner(corner) {
|
||
if (!this.selectedCellKey) return;
|
||
const cell = this.boardCells[this.selectedCellKey];
|
||
if (!cell || !cell.tileType) return;
|
||
this.pendingCorner = corner;
|
||
},
|
||
computeAllowedHouseCorners() {
|
||
if (!this.selectedCell || !this.selectedCell.tileType) {
|
||
this.allowedHouseCorners = [];
|
||
return;
|
||
}
|
||
this.allowedHouseCorners = this.getAllowedHouseCorners(this.selectedCell.tileType);
|
||
},
|
||
getAllowedHouseCorners(tileType) {
|
||
// lo=links oben, ro=rechts oben, lu=links unten, ru=rechts unten
|
||
switch (tileType) {
|
||
case 'cornerbottomright': return ['ro','lu','ru'];
|
||
case 'cornerbottomleft': return ['lo','lu','ru'];
|
||
case 'cornertopright': return ['lo','ro','ru'];
|
||
case 'cornertopleft': return ['lo','ro','lu'];
|
||
case 'fuelhorizontal': return ['ru','lu'];
|
||
case 'fuelvertical': return ['lo','lu'];
|
||
default: // horizontal, vertical, cross, tup, tdown, tleft, tright
|
||
return ['lo','ro','lu','ru'];
|
||
}
|
||
},
|
||
|
||
expandBoardIfNeeded(x, y, tileType) {
|
||
// Füge nur relevante Nachbarzellen basierend auf dem Tile-Typ hinzu
|
||
const neighborsToAdd = this.getRelevantNeighbors(x, y, tileType);
|
||
|
||
neighborsToAdd.forEach(neighbor => {
|
||
const key = `${neighbor.x},${neighbor.y}`;
|
||
if (!this.boardCells[key]) {
|
||
this.boardCells[key] = {
|
||
tileType: null,
|
||
x: neighbor.x,
|
||
y: neighbor.y
|
||
};
|
||
}
|
||
});
|
||
},
|
||
|
||
getRelevantNeighbors(x, y, tileType) {
|
||
const neighbors = [];
|
||
|
||
switch (tileType) {
|
||
case 'cornertopleft':
|
||
// Nur links und oben (Kurve von oben nach links)
|
||
neighbors.push({ x: x-1, y: y }, { x: x, y: y-1 });
|
||
break;
|
||
case 'cornertopright':
|
||
// Nur rechts und oben (Kurve von oben nach rechts)
|
||
neighbors.push({ x: x+1, y: y }, { x: x, y: y-1 });
|
||
break;
|
||
case 'cornerbottomleft':
|
||
// Nur links und unten (Kurve von unten nach links)
|
||
neighbors.push({ x: x-1, y: y }, { x: x, y: y+1 });
|
||
break;
|
||
case 'cornerbottomright':
|
||
// Nur rechts und unten (Kurve von unten nach rechts)
|
||
neighbors.push({ x: x+1, y: y }, { x: x, y: y+1 });
|
||
break;
|
||
case 'horizontal':
|
||
// Nur links und rechts (horizontal = nur horizontal anschließbar)
|
||
neighbors.push({ x: x-1, y: y }, { x: x+1, y: y });
|
||
break;
|
||
case 'vertical':
|
||
// Nur oben und unten (vertical = nur vertikal anschließbar)
|
||
neighbors.push({ x: x, y: y-1 }, { x: x, y: y+1 });
|
||
break;
|
||
case 'cross':
|
||
// Alle vier Richtungen (cross = überall anschließbar)
|
||
neighbors.push({ x: x-1, y: y }, { x: x+1, y: y }, { x: x, y: y-1 }, { x: x, y: y+1 });
|
||
break;
|
||
case 'fuelhorizontal':
|
||
// Nur links und rechts (wie horizontal)
|
||
neighbors.push({ x: x-1, y: y }, { x: x+1, y: y });
|
||
break;
|
||
case 'fuelvertical':
|
||
// Nur oben und unten (wie vertical)
|
||
neighbors.push({ x: x, y: y-1 }, { x: x, y: y+1 });
|
||
break;
|
||
case 'tup':
|
||
// Nur oben, links und rechts (T nach oben = 3 Anschlüsse)
|
||
neighbors.push({ x: x, y: y-1 }, { x: x-1, y: y }, { x: x+1, y: y });
|
||
break;
|
||
case 'tdown':
|
||
// Nur unten, links und rechts (T nach unten = 3 Anschlüsse)
|
||
neighbors.push({ x: x, y: y+1 }, { x: x-1, y: y }, { x: x+1, y: y });
|
||
break;
|
||
case 'tleft':
|
||
// Nur links, oben und unten (T nach links = 3 Anschlüsse)
|
||
neighbors.push({ x: x-1, y: y }, { x: x, y: y-1 }, { x: x, y: y+1 });
|
||
break;
|
||
case 'tright':
|
||
// Nur rechts, oben und unten (T nach rechts = 3 Anschlüsse)
|
||
neighbors.push({ x: x+1, y: y }, { x: x, y: y-1 }, { x: x, y: y+1 });
|
||
break;
|
||
default:
|
||
// Fallback: keine Nachbarn
|
||
return [];
|
||
}
|
||
|
||
return neighbors;
|
||
},
|
||
|
||
getCellClasses(cell, key) {
|
||
const classes = [];
|
||
|
||
if (this.selectedCellKey === key) {
|
||
classes.push('selected');
|
||
}
|
||
|
||
if (cell.tileType) {
|
||
classes.push('has-tile');
|
||
|
||
// Prüfe Tile-Kompatibilität
|
||
const compatibility = this.checkTileCompatibility(key, cell.tileType);
|
||
classes.push(compatibility);
|
||
} else {
|
||
classes.push('empty');
|
||
}
|
||
|
||
return classes;
|
||
},
|
||
|
||
checkTileCompatibility(cellKey, tileType) {
|
||
const [x, y] = cellKey.split(',').map(Number);
|
||
|
||
// Prüfe umgebende Tiles
|
||
const neighbors = {
|
||
top: this.getTileAt(x, y - 1),
|
||
right: this.getTileAt(x + 1, y),
|
||
bottom: this.getTileAt(x, y + 1),
|
||
left: this.getTileAt(x - 1, y)
|
||
};
|
||
|
||
// Wenn alle Nachbarn leer sind
|
||
if (Object.values(neighbors).every(tile => !tile)) {
|
||
return 'orange'; // Orange für leere Nachbarn
|
||
}
|
||
|
||
// Prüfe Kompatibilität mit vorhandenen Tiles
|
||
const isCompatible = this.isTileCompatible(tileType, neighbors);
|
||
|
||
return isCompatible ? 'green' : 'red';
|
||
},
|
||
|
||
getTileAt(x, y) {
|
||
const key = `${x},${y}`;
|
||
return this.boardCells[key]?.tileType;
|
||
},
|
||
|
||
getCellAtPosition(x, y) {
|
||
const key = `${x},${y}`;
|
||
const cell = this.boardCells[key];
|
||
if (cell) {
|
||
return cell;
|
||
}
|
||
// Erstelle leere Zelle für Grid-Position
|
||
return { tileType: null, x, y };
|
||
},
|
||
|
||
doorClass(deg) {
|
||
switch (deg) {
|
||
case 0: return 'door-bottom';
|
||
case 90: return 'door-right';
|
||
case 180: return 'door-top';
|
||
case 270: return 'door-left';
|
||
default: return 'door-bottom';
|
||
}
|
||
},
|
||
cornerClass(corner) {
|
||
switch (corner) {
|
||
case 'lo': return 'corner-lo';
|
||
case 'ro': return 'corner-ro';
|
||
case 'lu': return 'corner-lu';
|
||
case 'ru': return 'corner-ru';
|
||
default: return 'corner-lo';
|
||
}
|
||
},
|
||
|
||
range(start, end) {
|
||
const result = [];
|
||
for (let i = start; i <= end; i++) {
|
||
result.push(i);
|
||
}
|
||
return result;
|
||
},
|
||
|
||
isTileCompatible(tileType, neighbors) {
|
||
// Vereinfachte Kompatibilitätsprüfung
|
||
// In der Realität würde hier die komplexe Logik aus streetCoordinates verwendet
|
||
return true; // Für jetzt immer kompatibel
|
||
},
|
||
|
||
initializeBoard() {
|
||
// Starte mit einem leeren Tile in der Mitte (0,0)
|
||
this.boardCells = {
|
||
'0,0': {
|
||
tileType: null,
|
||
x: 0,
|
||
y: 0
|
||
}
|
||
};
|
||
},
|
||
|
||
createBoardFromData(mapData) {
|
||
this.boardCells = {};
|
||
|
||
// Konvertiere 2D Array zu Board-Zellen
|
||
for (let row = 0; row < mapData.length; row++) {
|
||
for (let col = 0; col < mapData[row].length; col++) {
|
||
const tileType = mapData[row][col];
|
||
if (tileType) {
|
||
const key = `${col},${row}`;
|
||
this.boardCells[key] = {
|
||
tileType: tileType,
|
||
x: col,
|
||
y: row
|
||
};
|
||
}
|
||
}
|
||
}
|
||
|
||
// Erweitere Board um leere Nachbarzellen
|
||
Object.keys(this.boardCells).forEach(key => {
|
||
const [x, y] = key.split(',').map(Number);
|
||
const cell = this.boardCells[key];
|
||
if (cell.tileType) {
|
||
this.expandBoardIfNeeded(x, y, cell.tileType);
|
||
}
|
||
});
|
||
},
|
||
|
||
generateMapData() {
|
||
if (Object.keys(this.boardCells).length === 0) {
|
||
return [];
|
||
}
|
||
|
||
const mapData = [];
|
||
for (let y = this.minY; y <= this.maxY; y++) {
|
||
const row = [];
|
||
for (let x = this.minX; x <= this.maxX; x++) {
|
||
const key = `${x},${y}`;
|
||
const cell = this.boardCells[key];
|
||
row.push(cell?.tileType || null);
|
||
}
|
||
mapData.push(row);
|
||
}
|
||
return mapData;
|
||
},
|
||
|
||
|
||
cancelEdit() {
|
||
this.editingMap = null;
|
||
this.selectedMapId = 'new';
|
||
this.selectedCellKey = null;
|
||
this.selectedTileType = null;
|
||
this.mapForm = {
|
||
name: '',
|
||
description: '',
|
||
mapData: []
|
||
};
|
||
this.initializeBoard();
|
||
},
|
||
|
||
async saveMap() {
|
||
try {
|
||
// Tiles in Array-Form bringen
|
||
const tiles = Object.values(this.boardCells)
|
||
.filter(c => !!c.tileType)
|
||
.map(c => ({
|
||
x: c.x,
|
||
y: c.y,
|
||
tileType: c.tileType,
|
||
meta: c.extraHouses ? { houses: { ...c.extraHouses } } : null
|
||
}));
|
||
const tileStreetNames = this.collectTileStreetNames();
|
||
const mapData = {
|
||
...this.mapForm,
|
||
width: this.boardWidth,
|
||
height: this.boardHeight,
|
||
tileSize: 50,
|
||
mapTypeId: 1,
|
||
tiles,
|
||
tileStreetNames
|
||
};
|
||
|
||
let savedMap;
|
||
const isUpdate = this.selectedMapId !== 'new';
|
||
|
||
if (isUpdate) {
|
||
// Map aktualisieren
|
||
const response = await apiClient.put(`/api/taxi-maps/maps/${this.selectedMapId}`, mapData);
|
||
savedMap = response.data.data;
|
||
} else {
|
||
// Neue Map erstellen
|
||
const response = await apiClient.post('/api/taxi-maps/maps', mapData);
|
||
savedMap = response.data.data;
|
||
}
|
||
|
||
this.editingMap = null;
|
||
this.selectedMapId = 'new';
|
||
this.selectedCellKey = null;
|
||
this.selectedTileType = null;
|
||
this.loadMaps();
|
||
|
||
// Erfolgsmeldung anzeigen
|
||
const message = isUpdate ? 'admin.taxiTools.mapEditor.updateSuccess' : 'admin.taxiTools.mapEditor.createSuccess';
|
||
this.$refs.messageDialog.open(`tr:${message}`);
|
||
} catch (error) {
|
||
console.error('Fehler beim Speichern der Map:', error);
|
||
alert('Fehler beim Speichern der Map');
|
||
}
|
||
},
|
||
|
||
collectTileStreetNames() {
|
||
const result = {};
|
||
for (const [key, cell] of Object.entries(this.boardCells)) {
|
||
if (!cell.tileType) continue;
|
||
const { streetNameH, streetNameV } = cell;
|
||
if (streetNameH || streetNameV) {
|
||
result[key] = { streetNameH: streetNameH || null, streetNameV: streetNameV || null };
|
||
}
|
||
}
|
||
return result;
|
||
},
|
||
|
||
async deleteMap(mapId) {
|
||
if (confirm('Möchtest du diese Map wirklich löschen?')) {
|
||
try {
|
||
await apiClient.delete(`/api/taxi-maps/maps/${mapId}`);
|
||
this.loadMaps();
|
||
|
||
// Erfolgsmeldung anzeigen
|
||
this.$refs.messageDialog.open('tr:admin.taxiTools.mapEditor.deleteSuccess');
|
||
} catch (error) {
|
||
console.error('Fehler beim Löschen der Map:', error);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.admin-header {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.admin-header h1 {
|
||
color: #F9A22C;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.admin-header p {
|
||
color: #666;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.taxi-tools-admin {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
.section-header {
|
||
margin-bottom: 30px;
|
||
text-align: center;
|
||
}
|
||
|
||
.section-header h2 {
|
||
color: #F9A22C;
|
||
font-size: 24px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
/* Map Selection */
|
||
.map-selection {
|
||
margin-bottom: 30px;
|
||
text-align: center;
|
||
}
|
||
|
||
.map-count {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.map-count p {
|
||
font-size: 18px;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.map-dropdown {
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.map-select {
|
||
padding: 10px 15px;
|
||
font-size: 16px;
|
||
border: 2px solid #ddd;
|
||
border-radius: 8px;
|
||
background: white;
|
||
min-width: 300px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.map-select:focus {
|
||
outline: none;
|
||
border-color: #F9A22C;
|
||
}
|
||
|
||
/* Map Details & Form */
|
||
.map-details,
|
||
.map-form {
|
||
background: white;
|
||
border: 2px solid #ddd;
|
||
border-radius: 12px;
|
||
padding: 25px;
|
||
margin-bottom: 30px;
|
||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.details-header,
|
||
.form-header {
|
||
margin-bottom: 25px;
|
||
text-align: center;
|
||
}
|
||
|
||
.details-header h3,
|
||
.form-header h3 {
|
||
color: #F9A22C;
|
||
font-size: 22px;
|
||
margin: 0;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.form-group input,
|
||
.form-group textarea,
|
||
.form-group select {
|
||
width: 100%;
|
||
padding: 12px;
|
||
border: 2px solid #ddd;
|
||
border-radius: 8px;
|
||
font-size: 16px;
|
||
transition: border-color 0.3s ease;
|
||
}
|
||
|
||
.form-group input:focus,
|
||
.form-group textarea:focus,
|
||
.form-group select:focus {
|
||
outline: none;
|
||
border-color: #F9A22C;
|
||
}
|
||
|
||
/* Map Editor Container */
|
||
.map-editor-container {
|
||
display: flex;
|
||
gap: 20px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
/* Game Board (links) */
|
||
.game-board {
|
||
flex: 1;
|
||
max-width: 600px;
|
||
}
|
||
|
||
|
||
.board-grid {
|
||
border: 2px solid #333;
|
||
padding: 10px;
|
||
background: #f5f5f5;
|
||
min-height: 200px;
|
||
}
|
||
|
||
.board-row {
|
||
display: contents;
|
||
}
|
||
|
||
.board-cell {
|
||
width: 50px;
|
||
height: 50px;
|
||
border: 1px solid #ccc;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
background: white;
|
||
transition: all 0.2s ease;
|
||
position: relative;
|
||
}
|
||
|
||
.board-cell:hover {
|
||
border-color: #F9A22C;
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
|
||
.board-cell.selected {
|
||
border: 3px solid #ffd700; /* Gelb für ausgewählte Zelle */
|
||
box-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
|
||
}
|
||
|
||
.board-cell.empty {
|
||
background: #f9f9f9;
|
||
}
|
||
|
||
.board-cell.has-tile {
|
||
background: #e8f5e8;
|
||
}
|
||
|
||
.board-cell.green {
|
||
border-color: #4CAF50;
|
||
box-shadow: 0 0 5px rgba(76, 175, 80, 0.5);
|
||
}
|
||
|
||
.board-cell.red {
|
||
border-color: #F44336;
|
||
box-shadow: 0 0 5px rgba(244, 67, 54, 0.5);
|
||
}
|
||
|
||
.board-cell.orange {
|
||
border-color: #FF9800;
|
||
box-shadow: 0 0 5px rgba(255, 152, 0, 0.5);
|
||
}
|
||
|
||
.tile-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: contain;
|
||
}
|
||
|
||
/* Tile Palette (rechts) */
|
||
.tile-palette {
|
||
flex: 0 0 300px;
|
||
}
|
||
|
||
/* Vereinheitlichte Tool-Panels rechts */
|
||
.tool-panel {
|
||
flex: 0 0 220px;
|
||
}
|
||
.tool-panel h4 {
|
||
margin: 0 0 8px 0; /* weniger Abstand oben/unten */
|
||
color: #F9A22C;
|
||
font-size: 18px;
|
||
}
|
||
.extra-grid { display: flex; gap: 8px; align-items: center; }
|
||
.extra-option {
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #fff;
|
||
cursor: pointer;
|
||
transition: transform 0.15s ease;
|
||
}
|
||
.extra-option:hover { transform: scale(1.05); }
|
||
.house-square { width: 10px; height: 10px; background: #d0d0d0; border: 1px solid #aaa; position: relative; }
|
||
.house-door { position: absolute; background: #000; }
|
||
.door-top { top: -1px; left: calc(50% - 2.5px); width: 5px; height: 2px; }
|
||
.door-bottom { bottom: -1px; left: calc(50% - 2.5px); width: 5px; height: 2px; }
|
||
.door-left { left: -1px; top: calc(50% - 2.5px); width: 2px; height: 5px; }
|
||
.door-right { right: -1px; top: calc(50% - 2.5px); width: 2px; height: 5px; }
|
||
.house-overlay { position: absolute; inset: 0; pointer-events: none; }
|
||
.house-square.corner-lo { position: absolute; top: 3px; left: 3px; }
|
||
.house-square.corner-ro { position: absolute; top: 3px; right: 3px; }
|
||
.house-square.corner-lu { position: absolute; bottom: 3px; left: 3px; }
|
||
.house-square.corner-ru { position: absolute; bottom: 3px; right: 3px; }
|
||
.corner-chooser { margin-top: 8px; display: flex; gap: 6px; }
|
||
.corner-btn { padding: 3px; border: 1px solid #ccc; border-radius: 4px; background: #f7f7f7; cursor: pointer; }
|
||
.corner-btn:hover { background: #eee; }
|
||
|
||
.corner-preview { width: 18px; height: 18px; display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
|
||
.corner-preview .q { width: 100%; height: 100%; background: #000; display: block; }
|
||
.corner-preview .q.active { background: #d60000; }
|
||
|
||
.panel-body.placeholder {
|
||
border: 2px dashed #ddd;
|
||
border-radius: 8px;
|
||
padding: 8px;
|
||
color: #999;
|
||
text-align: center;
|
||
}
|
||
.form-group-mini {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.arrow-label {
|
||
width: 28px;
|
||
text-align: center;
|
||
font-weight: 600;
|
||
}
|
||
.form-group-mini input {
|
||
flex: 1;
|
||
padding: 6px 8px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 6px;
|
||
}
|
||
.btn.mini {
|
||
padding: 6px 10px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.tile-palette h4 {
|
||
margin-bottom: 15px;
|
||
color: #F9A22C;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.tile-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 50px);
|
||
grid-template-rows: repeat(4, 50px);
|
||
gap: 5px;
|
||
border: 2px solid #ddd;
|
||
padding: 6px; /* weniger Innenabstand, damit oben/unten kompakter */
|
||
background: #f9f9f9;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.tile-option {
|
||
width: 50px;
|
||
height: 50px;
|
||
border: 2px solid #ddd;
|
||
border-radius: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
background: white;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.tile-option:hover {
|
||
border-color: #F9A22C;
|
||
transform: scale(1.1);
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.tile-option.selected {
|
||
border-color: #F9A22C;
|
||
background: #fff3cd;
|
||
box-shadow: 0 0 10px rgba(249, 162, 44, 0.5);
|
||
}
|
||
|
||
/* Form Actions */
|
||
.form-actions {
|
||
display: flex;
|
||
gap: 15px;
|
||
justify-content: center;
|
||
margin-top: 30px;
|
||
padding-top: 20px;
|
||
border-top: 1px solid #eee;
|
||
}
|
||
|
||
.btn {
|
||
padding: 12px 24px;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
text-decoration: none;
|
||
display: inline-block;
|
||
text-align: center;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #F9A22C;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: #e8941f;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #6c757d;
|
||
color: white;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: #5a6268;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.btn-danger {
|
||
background: #dc3545;
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background: #c82333;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
/* Responsive Design */
|
||
@media (max-width: 1200px) {
|
||
.map-editor-container {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.tile-palette {
|
||
flex: none;
|
||
max-width: none;
|
||
}
|
||
|
||
.tile-grid {
|
||
grid-template-columns: repeat(4, 50px);
|
||
grid-template-rows: repeat(4, 50px);
|
||
justify-content: center;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.taxi-tools-admin {
|
||
padding: 15px;
|
||
}
|
||
|
||
.form-actions {
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.map-select {
|
||
min-width: 250px;
|
||
}
|
||
|
||
.board-grid {
|
||
grid-template-columns: repeat(auto-fit, 40px) !important;
|
||
grid-template-rows: repeat(auto-fit, 40px) !important;
|
||
}
|
||
|
||
.board-cell {
|
||
width: 40px;
|
||
height: 40px;
|
||
}
|
||
|
||
.tile-grid {
|
||
grid-template-columns: repeat(4, 40px);
|
||
grid-template-rows: repeat(4, 40px);
|
||
}
|
||
|
||
.tile-option {
|
||
width: 40px;
|
||
height: 40px;
|
||
}
|
||
}
|
||
</style> |