410 lines
16 KiB
Vue
410 lines
16 KiB
Vue
<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>
|