Änderungen: - Implementierung des neuen Routers für TaxiHighscore zur Verwaltung von Highscore-Daten. - Anpassung der Datenbankmodelle zur Unterstützung von TaxiHighscore-Associations. - Erweiterung der Vue-Komponenten zur Anzeige und Speicherung von Highscores im Taxi-Spiel. - Verbesserung der Statusanzeige im AppHeader zur besseren Benutzerinteraktion. Diese Anpassungen erweitern die Spielmechanik und Benutzererfahrung, indem sie die Verwaltung von Highscores integrieren und die Benutzeroberfläche optimieren.
436 lines
15 KiB
Vue
436 lines
15 KiB
Vue
<template>
|
|
<div class="underground-view">
|
|
<StatusBar />
|
|
|
|
<h2>{{ $t('falukant.underground.title') }}</h2>
|
|
|
|
<SimpleTabs v-model="activeTab" :tabs="tabs" @change="onTabChange" />
|
|
|
|
<div class="tab-content">
|
|
<!-- Aktivitäten -->
|
|
<div v-if="activeTab === 'activities'" class="tab-pane">
|
|
<!-- Neues Activity-Formular -->
|
|
<div class="create-activity">
|
|
<h3>{{ $t('falukant.underground.activities.create') }}</h3>
|
|
|
|
<!-- Typ -->
|
|
<label class="form-label">
|
|
{{ $t('falukant.underground.activities.type') }}
|
|
<select v-model="newActivityTypeId" class="form-control">
|
|
<option v-for="type in undergroundTypes" :key="type.id" :value="type.id">
|
|
{{ $t(`falukant.underground.types.${type.tr}`) }}
|
|
({{ formatCost(type.cost) }})
|
|
</option>
|
|
</select>
|
|
</label>
|
|
|
|
<!-- Victim mit Autocomplete -->
|
|
<label class="form-label">
|
|
{{ $t('falukant.underground.activities.victim') }}
|
|
<input v-model="newVictimUsername" @input="onVictimInput" type="text" class="form-control"
|
|
:placeholder="$t('falukant.underground.activities.victimPlaceholder')" />
|
|
</label>
|
|
<div v-if="victimSuggestions.length" class="suggestions">
|
|
<ul>
|
|
<li v-for="s in victimSuggestions" :key="s.username" @click="selectVictim(s)">
|
|
{{ s.username }} — {{ s.firstname }} {{ s.lastname }}
|
|
({{ s.town }})
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Politische Zielpersonen (Multiselect) -->
|
|
<label v-if="selectedType && selectedType.tr === 'corrupt_politician'" class="form-label">
|
|
{{ $t('falukant.underground.activities.targets') }}
|
|
<select v-model="newPoliticalTargets" multiple size="5" class="form-control">
|
|
<option v-for="p in politicalTargets" :key="p.id" :value="p.id">
|
|
{{ $t('falukant.titles.' + p.gender + '.' + p.title) }}
|
|
{{ p.name }}
|
|
({{ $t('falukant.politics.offices.' + p.officeType) }})
|
|
</option>
|
|
</select>
|
|
</label>
|
|
|
|
<!-- Bei sabotage: Ziel auswählen -->
|
|
<label v-if="selectedType && selectedType.tr === 'sabotage'" class="form-label">
|
|
{{ $t('falukant.underground.activities.sabotageTarget') }}
|
|
<select v-model="newSabotageTarget" class="form-control">
|
|
<option value="house">
|
|
{{ $t('falukant.underground.targets.house') }}
|
|
</option>
|
|
<option value="storage">
|
|
{{ $t('falukant.underground.targets.storage') }}
|
|
</option>
|
|
</select>
|
|
</label>
|
|
|
|
<!-- Bei corrupt_politician: Ziel erreichen -->
|
|
<label v-if="selectedType && selectedType.tr === 'corrupt_politician'" class="form-label">
|
|
{{ $t('falukant.underground.activities.corruptGoal') }}
|
|
<select v-model="newCorruptGoal" class="form-control">
|
|
<option value="elect">
|
|
{{ $t('falukant.underground.goals.elect') }}
|
|
</option>
|
|
<option value="tax_increase">
|
|
{{ $t('falukant.underground.goals.taxIncrease') }}
|
|
</option>
|
|
<option value="tax_decrease">
|
|
{{ $t('falukant.underground.goals.taxDecrease') }}
|
|
</option>
|
|
</select>
|
|
</label>
|
|
|
|
<button class="btn-create-activity" :disabled="!canCreate" @click="createActivity">
|
|
{{ $t('falukant.underground.activities.create') }}
|
|
</button>
|
|
</div>
|
|
<!-- /Neues Activity-Formular -->
|
|
|
|
<div v-if="loading.activities" class="loading">
|
|
{{ $t('loading') }}
|
|
</div>
|
|
<div v-else class="activities-table">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>{{ $t('falukant.underground.activities.type') }}</th>
|
|
<th>{{ $t('falukant.underground.activities.victim') }}</th>
|
|
<th>{{ $t('falukant.underground.activities.cost') }}</th>
|
|
<th>{{ $t('falukant.underground.activities.additionalInfo') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="act in activities" :key="act.id">
|
|
<td>{{ $t(`falukant.underground.types.${act.type}`) }}</td>
|
|
<td>{{ act.victimName }}</td>
|
|
<td>{{ formatCost(act.cost) }}</td>
|
|
<td>
|
|
<template v-if="act.type === 'sabotage'">
|
|
{{ $t(`falukant.underground.targets.${act.target}`) }}
|
|
</template>
|
|
<template v-else-if="act.type === 'corrupt_politician'">
|
|
{{ $t(`falukant.underground.goals.${act.goal}`) }}
|
|
</template>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="!activities.length">
|
|
<td colspan="4">{{ $t('falukant.underground.activities.none') }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Angriffe -->
|
|
<div v-else-if="activeTab === 'attacks'" class="tab-pane">
|
|
<div v-if="loading.attacks" class="loading">
|
|
{{ $t('loading') }}
|
|
</div>
|
|
<div v-else class="attacks-list">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>{{ $t('falukant.underground.attacks.source') }}</th>
|
|
<th>{{ $t('falukant.underground.attacks.date') }}</th>
|
|
<th>{{ $t('falukant.underground.attacks.success') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="atk in attacks" :key="atk.id">
|
|
<td>{{ atk.targetName }}</td>
|
|
<td>{{ formatDate(atk.date) }}</td>
|
|
<td>{{ atk.success ? $t('yes') : $t('no') }}</td>
|
|
</tr>
|
|
<tr v-if="!attacks.length">
|
|
<td colspan="3">
|
|
{{ $t('falukant.underground.attacks.none') }}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import StatusBar from '@/components/falukant/StatusBar.vue';
|
|
import SimpleTabs from '@/components/SimpleTabs.vue';
|
|
import apiClient from '@/utils/axios.js';
|
|
|
|
export default {
|
|
name: 'UndergroundView',
|
|
components: { StatusBar, SimpleTabs },
|
|
data() {
|
|
return {
|
|
activeTab: 'activities',
|
|
tabs: [
|
|
{ value: 'activities', label: 'falukant.underground.tabs.activities' },
|
|
{ value: 'attacks', label: 'falukant.underground.tabs.attacks' }
|
|
],
|
|
undergroundTypes: [],
|
|
activities: [],
|
|
attacks: [],
|
|
loading: { activities: false, attacks: false },
|
|
newActivityTypeId: null,
|
|
newVictimUsername: '',
|
|
victimSuggestions: [],
|
|
victimSearchTimeout: null,
|
|
newPoliticalTargets: [],
|
|
newSabotageTarget: 'house',
|
|
newCorruptGoal: 'elect'
|
|
};
|
|
},
|
|
computed: {
|
|
selectedType() {
|
|
return this.undergroundTypes.find(
|
|
t => t.id === this.newActivityTypeId
|
|
) || null;
|
|
},
|
|
canCreate() {
|
|
if (!this.newActivityTypeId) return false;
|
|
const hasUser = this.newVictimUsername.trim().length > 0;
|
|
const hasPol = this.newPoliticalTargets.length > 0;
|
|
if (!hasUser && !hasPol) return false;
|
|
if (this.selectedType?.tr === 'sabotage' && !this.newSabotageTarget) {
|
|
return false;
|
|
}
|
|
if (
|
|
this.selectedType?.tr === 'corrupt_politician' &&
|
|
!this.newCorruptGoal
|
|
) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
},
|
|
async mounted() {
|
|
await this.loadUndergroundTypes();
|
|
if (this.undergroundTypes.length) {
|
|
this.newActivityTypeId = this.undergroundTypes[0].id;
|
|
}
|
|
await this.loadActivities();
|
|
},
|
|
methods: {
|
|
onTabChange(tab) {
|
|
if (tab === 'activities' && !this.activities.length) {
|
|
this.loadActivities();
|
|
}
|
|
if (tab === 'attacks' && !this.attacks.length) {
|
|
this.loadAttacks();
|
|
}
|
|
},
|
|
|
|
onVictimInput() {
|
|
clearTimeout(this.victimSearchTimeout);
|
|
const q = this.newVictimUsername.trim();
|
|
if (q.length >= 3) {
|
|
this.victimSearchTimeout = setTimeout(() => {
|
|
this.searchVictims(q);
|
|
}, 300);
|
|
} else {
|
|
this.victimSuggestions = [];
|
|
}
|
|
},
|
|
async searchVictims(q) {
|
|
console.log('Searching victims for:', q);
|
|
try {
|
|
const { data } = await apiClient.get('/api/falukant/users/search', {
|
|
params: { q }
|
|
});
|
|
this.victimSuggestions = data;
|
|
} catch (err) {
|
|
console.error('Error searching users', err);
|
|
}
|
|
},
|
|
selectVictim(u) {
|
|
this.newVictimUsername = u.username;
|
|
this.victimSuggestions = [];
|
|
},
|
|
|
|
async createActivity() {
|
|
if (!this.canCreate) return;
|
|
const payload = {
|
|
typeId: this.newActivityTypeId,
|
|
victimUsername: this.newVictimUsername.trim()
|
|
};
|
|
if (this.selectedType.tr === 'sabotage') {
|
|
payload.target = this.newSabotageTarget;
|
|
}
|
|
if (this.selectedType.tr === 'corrupt_politician') {
|
|
payload.goal = this.newCorruptGoal;
|
|
if (this.newPoliticalTargets.length) {
|
|
payload.politicalTargets = this.newPoliticalTargets;
|
|
}
|
|
}
|
|
try {
|
|
await apiClient.post(
|
|
'/api/falukant/underground/activities',
|
|
payload
|
|
);
|
|
this.newVictimUsername = '';
|
|
this.newPoliticalTargets = [];
|
|
this.newSabotageTarget = 'house';
|
|
this.newCorruptGoal = 'elect';
|
|
await this.loadActivities();
|
|
} catch (err) {
|
|
console.error('Error creating activity', err);
|
|
}
|
|
},
|
|
|
|
async loadUndergroundTypes() {
|
|
const { data } = await apiClient.get(
|
|
'/api/falukant/underground/types'
|
|
);
|
|
this.undergroundTypes = data;
|
|
},
|
|
|
|
async loadActivities() {
|
|
return; // TODO: Aktivierung der Methode geplant
|
|
/* Temporär deaktiviert:
|
|
this.loading.activities = true;
|
|
try {
|
|
const { data } = await apiClient.get(
|
|
'/api/falukant/underground/activities'
|
|
);
|
|
this.activities = data;
|
|
} finally {
|
|
this.loading.activities = false;
|
|
}
|
|
*/
|
|
},
|
|
|
|
async loadAttacks() {
|
|
this.loading.attacks = true;
|
|
try {
|
|
const { data } = await apiClient.get(
|
|
'/api/falukant/underground/attacks'
|
|
);
|
|
this.attacks = data;
|
|
} finally {
|
|
this.loading.attacks = false;
|
|
}
|
|
},
|
|
|
|
formatDate(ts) {
|
|
return new Date(ts).toLocaleDateString(this.$i18n.locale, {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
},
|
|
|
|
formatCost(v) {
|
|
return new Intl.NumberFormat(navigator.language, {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0
|
|
}).format(v);
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
h2 {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.underground-view {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.tab-content {
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.tab-pane {
|
|
min-height: 200px;
|
|
}
|
|
|
|
.loading {
|
|
font-style: italic;
|
|
text-align: center;
|
|
margin: 1em 0;
|
|
}
|
|
|
|
.create-activity {
|
|
border: 1px solid #ccc;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
background: #fafafa;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.form-label {
|
|
display: block;
|
|
margin-bottom: 0.5rem;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.form-control {
|
|
display: block;
|
|
width: 100%;
|
|
padding: 0.4rem;
|
|
margin-bottom: 1rem;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.btn-create-activity {
|
|
padding: 0.5rem 1rem;
|
|
background: #4caf50;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.btn-create-activity:disabled {
|
|
background: #ccc;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.activities-table table,
|
|
.attacks-list table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.activities-table th,
|
|
.activities-table td,
|
|
.attacks-list th,
|
|
.attacks-list td {
|
|
padding: 8px;
|
|
border: 1px solid #ddd;
|
|
text-align: left;
|
|
}
|
|
|
|
.suggestions {
|
|
position: absolute;
|
|
background: #fff;
|
|
border: 1px solid #ccc;
|
|
z-index: 10;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.suggestions ul {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.suggestions li {
|
|
padding: 0.5em;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.suggestions li:hover {
|
|
background: #eee;
|
|
}
|
|
</style>
|