Files
yourpart3/frontend/src/components/falukant/StatusBar.vue
Torsten Schulz (local) ce71f6d8ea feat(StatusBar, falukantService): enhance character information display and UI structure
- Updated falukantService to include predefined first and last names in character data retrieval.
- Refactored StatusBar component layout to improve organization and visual clarity, introducing sections for identity and stats.
- Enhanced character name handling to prioritize display names, ensuring a more dynamic user experience.
2026-03-30 10:31:59 +02:00

512 lines
19 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="statusbar-main">
<div class="statusbar-section statusbar-section--identity">
<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>
<div v-if="characterName" class="status-identity" :title="characterName">
<span class="status-identity__label">{{ $t('falukant.overview.metadata.name') }}</span>
<strong class="status-identity__name">{{ characterName }}</strong>
</div>
</div>
<div class="statusbar-section statusbar-section--stats">
<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>
<div
v-if="statusItems.length > 0 && menu.falukant && menu.falukant.children"
class="statusbar-section statusbar-section--nav"
>
<div 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>
</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
},
characterName: '',
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 firstName = character?.definedFirstName?.name || character?.firstName || '';
const lastName = character?.definedLastName?.name || character?.lastName || '';
const explicitName = character?.displayName || character?.name || '';
this.characterName = explicitName || [firstName, lastName].filter(Boolean).join(' ');
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 {
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%;
padding: 0.55rem 0.9rem;
margin: 0 0 1.5em 0;
position: sticky;
top: 0;
z-index: 100;
box-shadow: var(--shadow-soft);
}
.statusbar-main {
display: grid;
grid-template-columns: minmax(15rem, 1.1fr) minmax(0, 1.5fr) minmax(14rem, 1fr);
align-items: center;
gap: 0.85rem 1rem;
width: 100%;
}
.statusbar-section {
min-width: 0;
}
.statusbar-section--identity {
display: flex;
align-items: center;
gap: 0.7rem;
min-width: 0;
}
.statusbar-section--stats {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 0.55rem;
min-width: 0;
}
.statusbar-section--nav {
display: flex;
justify-content: flex-end;
min-width: 0;
}
.statusbar-warning {
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;
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);
}
.status-item.messages {
cursor: pointer;
flex: 0 0 auto;
}
.status-identity {
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.15rem;
min-height: 50px;
padding: 0.45rem 1rem;
border-radius: var(--radius-md);
background: linear-gradient(180deg, rgba(255,255,255,0.94) 0%, rgba(248, 241, 231, 0.98) 100%);
border: 1px solid rgba(126, 71, 27, 0.16);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.65);
min-width: 0;
flex: 1 1 auto;
}
.status-identity__label {
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #9a7a5f;
white-space: nowrap;
}
.status-identity__name {
font-size: 1rem;
line-height: 1.2;
color: #5f3617;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.quick-access {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 0.15rem;
min-width: 0;
}
.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;
}
@media (max-width: 1180px) {
.statusbar-main {
grid-template-columns: 1fr;
}
.statusbar-section--stats {
justify-content: flex-start;
}
.statusbar-section--nav {
justify-content: flex-start;
}
.quick-access {
justify-content: flex-start;
}
}
</style>