Files
yourpart3/frontend/src/components/falukant/StatusBar.vue

410 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="statusbar">
<div class="status-item messages" @click="openMessages" :title="$t('falukant.messages.tooltip')">
<span class="badge" v-if="unreadCount > 0">{{ unreadCount }}</span>
<img src="/images/icons/falukant/messages24.png" class="menu-icon" />
</div>
<template v-for="item in statusItems" :key="item.key">
<div class="status-item" v-if="item.value !== null && item.image == null">
<span class="status-icon-wrapper">
<template v-if="item.iconImage">
<img :src="'/images/icons/' + item.iconImage" class="inline-icon" width="16" height="16" :title="String(item.value)" />
</template>
<template v-else>
<span class="status-icon-symbol" :title="String(item.value)">{{ item.icon }}</span>
</template>
</span>
<span class="status-label" :title="$t(`falukant.statusbar.${item.key}`)">{{ item.value }}</span>
</div>
<div class="status-item" v-else-if="item.image !== null">
<span class="status-icon-wrapper">
<span class="status-icon-symbol" :title="$t(`falukant.statusbar.${item.key}`)">{{ item.icon }}</span>
</span>
<img :src="'/images/icons/falukant/relationship-' + item.image + '.png'" class="relationship-icon" :title="$t(`falukant.statusbar.${item.key}`)" />
</div>
</template>
<div
v-if="statusItems.length > 0 && menu.falukant && menu.falukant.children"
class="quick-access"
>
<template v-for="(menuItem, key) in menu.falukant.children" :key="menuItem.id" >
<img
:src="'/images/icons/falukant/shortmap/' + key + '.png'"
class="menu-icon"
@click="openPage(menuItem)"
:title="$t(`navigation.m-falukant.${key}`)"
/>
</template>
</div>
<div v-if="debtorsPrison.active" class="statusbar-warning" :class="{ 'is-prison': debtorsPrison.inDebtorsPrison }">
<strong>
{{ debtorsPrison.inDebtorsPrison
? $t('falukant.bank.debtorsPrison.titlePrison')
: $t('falukant.bank.debtorsPrison.titleWarning') }}
</strong>
<span>
{{ debtorsPrison.inDebtorsPrison
? $t('falukant.debtorsPrison.globalLocked')
: $t('falukant.debtorsPrison.globalWarning') }}
</span>
</div>
<MessagesDialog ref="msgs" />
</div>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import apiClient from "@/utils/axios.js";
import { EventBus } from '@/utils/eventBus.js';
import MessagesDialog from './MessagesDialog.vue';
export default {
name: "StatusBar",
components: { MessagesDialog },
data() {
return {
statusItems: [
{ key: "age", icon: null, iconImage: 'falukant/age.png', value: 0 },
{ key: "relationship", icon: "💑", image: null },
{ key: "wealth", icon: "💰", value: 0 },
{ key: "health", icon: "❤️", value: "Good" },
{ key: "events", icon: "📰", value: null, image: null },
{ key: "children", icon: "👶", value: null },
],
unreadCount: 0,
debtorsPrison: {
active: false,
inDebtorsPrison: false
},
pendingStatusRefresh: null,
};
},
computed: {
...mapState(["socket", "daemonSocket", "user"]),
...mapGetters(['menu']),
},
watch: {
// Wenn sich das Menü ändert, lade die Bilder neu
'menu.falukant': {
handler() {
this.preloadQuickAccessImages();
},
deep: true
},
socket(newVal, oldVal) {
if (oldVal) this.teardownSocketListeners();
if (newVal) this.setupSocketListeners();
},
daemonSocket(newVal, oldVal) {
if (oldVal && this._daemonHandler) {
oldVal.removeEventListener('message', this._daemonHandler);
this._daemonHandler = null;
}
if (newVal) this.setupDaemonListeners();
}
},
async mounted() {
await this.fetchStatus();
// Bilder für Schnellzugriff vorladen und cachen
this.preloadQuickAccessImages();
// Socket.IO (Backend notifyUser) Hauptkanal für Falukant-Events
this.setupSocketListeners();
this.setupDaemonListeners();
EventBus.on('open-falukant-messages', this.openMessages);
},
beforeUnmount() {
this.teardownSocketListeners();
this.teardownDaemonListeners();
if (this.pendingStatusRefresh) {
clearTimeout(this.pendingStatusRefresh);
this.pendingStatusRefresh = null;
}
EventBus.off('open-falukant-messages', this.openMessages);
},
methods: {
preloadQuickAccessImages() {
// Lade alle Schnellzugriffs-Bilder vor, damit sie gecacht werden
if (this.menu.falukant && this.menu.falukant.children) {
Object.keys(this.menu.falukant.children).forEach(key => {
const img = new Image();
img.src = `/images/icons/falukant/shortmap/${key}.png`;
});
}
// Lade auch andere häufig verwendete Bilder vor
const commonImages = [
'/images/icons/falukant/messages24.png',
'/images/icons/falukant/age.png',
// Relationship-Bilder vorladen
'/images/icons/falukant/relationship-engaged.png',
'/images/icons/falukant/relationship-wooing.png',
'/images/icons/falukant/relationship-married.png',
'/images/icons/falukant/relationship-widow.png',
'/images/icons/falukant/relationship-.png'
];
commonImages.forEach(src => {
const img = new Image();
img.src = src;
});
},
async fetchStatus() {
try {
const response = await apiClient.get("/api/falukant/info");
const { money, character, events } = response.data;
const { age, health } = character;
const relationship = response.data.character.relationshipsAsCharacter1[0]?.relationshipType?.tr
|| response.data.character.relationshipsAsCharacter2[0]?.relationshipType?.tr
|| null;
// Children counts and unread notifications are now part of /info
const childCount = Number(response.data.childrenCount) || 0;
const unbaptisedCount = Number(response.data.unbaptisedChildrenCount) || 0;
this.unreadCount = Number(response.data.unreadNotifications) || 0;
this.debtorsPrison = response.data.debtorsPrison || {
active: false,
inDebtorsPrison: false
};
const childrenDisplay = `${childCount}${unbaptisedCount > 0 ? `(${unbaptisedCount})` : ''}`;
let healthStatus = '';
if (health > 90) {
healthStatus = this.$t("falukant.health.amazing");
} else if (health > 75) {
healthStatus = this.$t("falukant.health.good");
} else if (health > 50) {
healthStatus = this.$t("falukant.health.normal");
} else if (health > 25) {
healthStatus = this.$t("falukant.health.bad");
} else {
healthStatus = this.$t("falukant.health.very_bad");
}
this.statusItems = [
{ key: "age", icon: null, iconImage: 'falukant/age.png', value: age },
{ key: "relationship", icon: "💑", image: relationship },
{ key: "wealth", icon: "💰", value: Intl.NumberFormat(navigator.language, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(money) },
{ key: "health", icon: "❤️", value: healthStatus },
{ key: "events", icon: "📰", value: events || null, image: null },
{ key: "children", icon: "👶", value: childrenDisplay },
];
} catch (error) {
// Error fetching status
}
},
setupSocketListeners() {
this.teardownSocketListeners();
if (!this.socket) return;
this._statusSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data });
this._familySocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateFamily', ...data });
this._churchSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateChurch', ...data });
this._debtSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateDebt', ...data });
this._childrenSocketHandler = (data) => this.handleEvent({ event: 'children_update', ...data });
this._productionCertificateSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateProductionCertificate', ...data });
this._stockSocketHandler = (data) => this.handleEvent({ event: 'stock_change', ...data });
this._familyChangedSocketHandler = (data) => this.handleEvent({ event: 'familychanged', ...data });
this.socket.on('falukantUpdateStatus', this._statusSocketHandler);
this.socket.on('falukantUpdateFamily', this._familySocketHandler);
this.socket.on('falukantUpdateChurch', this._churchSocketHandler);
this.socket.on('falukantUpdateDebt', this._debtSocketHandler);
this.socket.on('children_update', this._childrenSocketHandler);
this.socket.on('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
this.socket.on('stock_change', this._stockSocketHandler);
this.socket.on('familychanged', this._familyChangedSocketHandler);
},
teardownSocketListeners() {
if (this.socket) {
if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler);
if (this._familySocketHandler) this.socket.off('falukantUpdateFamily', this._familySocketHandler);
if (this._churchSocketHandler) this.socket.off('falukantUpdateChurch', this._churchSocketHandler);
if (this._debtSocketHandler) this.socket.off('falukantUpdateDebt', this._debtSocketHandler);
if (this._childrenSocketHandler) this.socket.off('children_update', this._childrenSocketHandler);
if (this._productionCertificateSocketHandler) this.socket.off('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
if (this._stockSocketHandler) this.socket.off('stock_change', this._stockSocketHandler);
if (this._familyChangedSocketHandler) this.socket.off('familychanged', this._familyChangedSocketHandler);
}
},
setupDaemonListeners() {
this.teardownDaemonListeners();
if (!this.daemonSocket) return;
this._daemonHandler = (event) => {
try {
const data = JSON.parse(event.data);
if (['falukantUpdateStatus', 'falukantUpdateFamily', 'falukantUpdateChurch', 'falukantUpdateDebt', 'children_update', 'falukantUpdateProductionCertificate', 'stock_change', 'familychanged'].includes(data.event)) {
this.handleEvent(data);
}
} catch (_) {}
};
this.daemonSocket.addEventListener('message', this._daemonHandler);
},
matchesCurrentUser(eventData) {
if (eventData?.user_id == null) {
return true;
}
const currentIds = [this.user?.id, this.user?.hashedId]
.filter(Boolean)
.map((value) => String(value));
return currentIds.includes(String(eventData.user_id));
},
teardownDaemonListeners() {
const sock = this.daemonSocket;
if (sock && this._daemonHandler) {
sock.removeEventListener('message', this._daemonHandler);
this._daemonHandler = null;
}
},
queueStatusRefresh() {
if (this.pendingStatusRefresh) {
clearTimeout(this.pendingStatusRefresh);
}
this.pendingStatusRefresh = setTimeout(async () => {
this.pendingStatusRefresh = null;
await this.fetchStatus();
}, 120);
},
handleEvent(eventData) {
if (!this.matchesCurrentUser(eventData)) {
return;
}
switch (eventData.event) {
case 'falukantUpdateStatus':
case 'falukantUpdateFamily':
case 'falukantUpdateChurch':
case 'falukantUpdateDebt':
case 'children_update':
case 'falukantUpdateProductionCertificate':
case 'stock_change':
case 'familychanged':
this.queueStatusRefresh();
break;
}
},
openPage(url, hasSubmenu = false) {
if (hasSubmenu) {
return;
}
if (url) {
this.$router.push(url);
}
},
openMessages() {
this.$refs.msgs.open();
// After opening, refresh unread count after a short delay (server marks them shown)
setTimeout(() => this.fetchStatus(), 500);
},
},
};
</script>
<style scoped>
.statusbar {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
background:
linear-gradient(180deg, rgba(255, 251, 246, 0.96) 0%, rgba(247, 238, 224, 0.98) 100%);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-sizing: border-box;
width: 100%;
max-width: 100%;
gap: 1.2em;
padding: 0.55rem 0.9rem;
margin: 0 0 1.5em 0;
position: sticky;
top: 0;
z-index: 100;
box-shadow: var(--shadow-soft);
}
.statusbar-warning {
flex: 1 1 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: var(--radius-md);
border: 1px solid rgba(180, 120, 40, 0.35);
background: rgba(255, 244, 223, 0.92);
color: #8a5411;
font-size: 0.92rem;
}
.statusbar-warning.is-prison {
border-color: rgba(146, 57, 40, 0.42);
background: rgba(255, 232, 225, 0.94);
color: #8b2f23;
}
.status-item {
text-align: center;
cursor: pointer;
display: inline-flex;
align-items: center;
min-height: 34px;
padding: 0 10px;
border-radius: 999px;
background: rgba(255,255,255,0.68);
border: 1px solid rgba(93, 64, 55, 0.08);
}
.quick-access {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.2rem;
}
.status-icon-wrapper {
display: inline-flex;
align-items: center;
margin-right: 4px;
}
.status-icon-symbol {
font-size: 14px;
}
.status-label {
font-size: 14px;
}
.menu-icon {
width: 30px;
height: 30px;
display: block;
flex: 0 0 auto;
cursor: pointer;
padding: 4px 2px 0 0;
}
.relationship-icon {
max-width: 24px;
max-height: 24px;
}
.messages { position: relative; }
.badge {
position: absolute;
top: -6px;
right: -2px;
background: #e53935;
color: #fff;
border-radius: 10px;
padding: 0 6px;
font-size: 12px;
line-height: 18px;
min-width: 18px;
text-align: center;
}
.inline-icon {
width: 16px;
height: 16px;
vertical-align: middle;
margin-right: 4px;
}
</style>