103 lines
3.3 KiB
Vue
103 lines
3.3 KiB
Vue
<template>
|
|
<component :is="isClickable() ? 'button' : 'div'" :type="isClickable() ? 'button' : undefined"
|
|
:class="['adm-stat', `adm-tone-${toneName}`, { 'adm-tone-custom': !!color }]"
|
|
:style="color ? { '--adm-tone': color } : null"
|
|
@click="onClick">
|
|
<span v-if="icon" class="adm-stat__chip" aria-hidden="true"><Icon :icon="icon"/></span>
|
|
<span class="adm-stat__info">
|
|
<span class="adm-stat__label">{{ label }}</span>
|
|
<span :class="['adm-stat__value', { 'adm-stat__value--mono': mono }]"><slot>{{ value }}</slot></span>
|
|
<span v-if="sub || $slots.sub" class="adm-stat__sub"><slot name="sub">{{ sub }}</slot></span>
|
|
<AdminMeter v-if="hasProgress" :value="progress" :label="progressLabel || label" :tone="progressTone"/>
|
|
</span>
|
|
<template v-if="isClickable()">
|
|
<Icon class="adm-stat__go" icon="fa-solid fa-chevron-right" aria-hidden="true"/>
|
|
<span v-if="to" class="sr-only">, ouvrir la section {{ targetLabel }}</span>
|
|
</template>
|
|
</component>
|
|
</template>
|
|
<script setup>
|
|
/**
|
|
* Tuile de chiffre clé. `to` (id de section) la rend cliquable (bouton → adminStore.setSection),
|
|
* tout comme un écouteur @click. `progress` (0..1) ajoute une jauge sous la valeur.
|
|
*/
|
|
import { computed, inject, useAttrs } from 'vue';
|
|
import { useAdminStore } from '@/stores/adminStore';
|
|
import AdminMeter from './AdminMeter.vue';
|
|
import { ADMIN_NAV_KEY, normalizeTone, toneValidator } from './adminUi';
|
|
import './admin.scss';
|
|
|
|
const props = defineProps({
|
|
label: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
value: {
|
|
default: 0
|
|
},
|
|
sub: {
|
|
type: String,
|
|
default: null
|
|
},
|
|
icon: {
|
|
type: String,
|
|
default: "fa-solid fa-chart-bar"
|
|
},
|
|
// Historique : couleur libre de la pastille (prioritaire sur `tone`)
|
|
color: {
|
|
type: String,
|
|
default: null
|
|
},
|
|
tone: {
|
|
type: String,
|
|
default: 'default',
|
|
validator: toneValidator
|
|
},
|
|
// Id de section ('players', 'users'…) : la tuile ouvre cette section
|
|
to: {
|
|
type: String,
|
|
default: null
|
|
},
|
|
// Libellé lisible de la section cible, pour les lecteurs d'écran
|
|
toLabel: {
|
|
type: String,
|
|
default: null
|
|
},
|
|
progress: {
|
|
type: Number,
|
|
default: null
|
|
},
|
|
progressLabel: {
|
|
type: String,
|
|
default: null
|
|
},
|
|
// 'auto' : rose, orange au-delà de 75 %, rouge au-delà de 90 %
|
|
progressTone: {
|
|
type: String,
|
|
default: 'auto'
|
|
},
|
|
mono: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
});
|
|
|
|
const attrs = useAttrs();
|
|
const adminStore = useAdminStore();
|
|
const nav = inject(ADMIN_NAV_KEY, null);
|
|
|
|
const toneName = computed(() => normalizeTone(props.tone));
|
|
// Pas de defineEmits('click') : l'écouteur du parent reste dans attrs et passe au bouton racine.
|
|
// useAttrs() n'est pas réactif : fonction relue à chaque rendu plutôt qu'un computed
|
|
function isClickable() {
|
|
return !!props.to || !!attrs.onClick;
|
|
}
|
|
|
|
const hasProgress = computed(() => typeof props.progress === 'number' && Number.isFinite(props.progress));
|
|
const targetLabel = computed(() => props.toLabel || nav?.sections?.find(s => s.id === props.to)?.label || props.to);
|
|
|
|
function onClick() {
|
|
if (props.to) adminStore.setSection(props.to);
|
|
}
|
|
</script>
|