152 lines
3.0 KiB
Vue
152 lines
3.0 KiB
Vue
<script setup lang="ts">
|
|
import UiIcon from '@/components/UiIcon.vue';
|
|
|
|
type IconName = InstanceType<typeof UiIcon>['$props']['name'];
|
|
|
|
withDefaults(
|
|
defineProps<{
|
|
label: string;
|
|
value: string;
|
|
hint?: string;
|
|
icon?: IconName;
|
|
tone?: 'accent' | 'success' | 'warning' | 'danger' | 'info' | 'neutral';
|
|
/** تغییر نسبت به دوره قبل (درصد یا عدد)؛ مثبت = بهتر */
|
|
delta?: number | null;
|
|
deltaSuffix?: string;
|
|
}>(),
|
|
{ hint: '', icon: undefined, tone: 'neutral', delta: null, deltaSuffix: '' },
|
|
);
|
|
|
|
function fa(n: number) {
|
|
return Math.abs(n).toLocaleString('fa-IR');
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="kpi" :class="`tone-${tone}`">
|
|
<div class="kpi-top">
|
|
<span class="kpi-label">{{ label }}</span>
|
|
<span v-if="icon" class="kpi-ico"><UiIcon :name="icon" :size="16" /></span>
|
|
</div>
|
|
<strong class="kpi-value">{{ value }}</strong>
|
|
<div v-if="hint || delta != null" class="kpi-foot">
|
|
<span v-if="delta != null && delta !== 0" class="delta" :class="delta > 0 ? 'up' : 'down'">
|
|
{{ delta > 0 ? '▲' : '▼' }} {{ fa(delta) }}{{ deltaSuffix }}
|
|
</span>
|
|
<span v-else-if="delta === 0" class="delta flat">بدون تغییر</span>
|
|
<span v-if="hint" class="kpi-hint">{{ hint }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.kpi {
|
|
display: grid;
|
|
gap: 6px;
|
|
padding: var(--sp-4);
|
|
background: var(--surface);
|
|
border: 1px solid var(--line);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow-xs);
|
|
min-width: 0;
|
|
}
|
|
|
|
.kpi-top {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: var(--sp-2);
|
|
}
|
|
|
|
.kpi-label {
|
|
font-size: var(--fs-sm);
|
|
color: var(--muted);
|
|
font-weight: var(--fw-medium);
|
|
}
|
|
|
|
.kpi-ico {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 8px;
|
|
display: grid;
|
|
place-items: center;
|
|
background: var(--surface-subtle);
|
|
color: var(--ink-secondary);
|
|
}
|
|
|
|
.tone-accent .kpi-ico {
|
|
background: var(--accent-subtle);
|
|
color: var(--accent-deep);
|
|
}
|
|
|
|
.tone-success .kpi-ico {
|
|
background: var(--success-soft);
|
|
color: var(--success-deep);
|
|
}
|
|
|
|
.tone-warning .kpi-ico {
|
|
background: var(--warning-soft);
|
|
color: var(--warning-deep);
|
|
}
|
|
|
|
.tone-danger .kpi-ico {
|
|
background: var(--danger-soft);
|
|
color: var(--danger);
|
|
}
|
|
|
|
.tone-info .kpi-ico {
|
|
background: var(--info-soft);
|
|
color: var(--info-deep);
|
|
}
|
|
|
|
.kpi-value {
|
|
font-size: var(--fs-2xl);
|
|
font-weight: var(--fw-bold);
|
|
color: var(--ink);
|
|
line-height: 1.2;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.tone-danger .kpi-value {
|
|
color: var(--danger);
|
|
}
|
|
|
|
.kpi-foot {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
font-size: var(--fs-xs);
|
|
}
|
|
|
|
.kpi-hint {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.delta {
|
|
font-weight: var(--fw-semibold);
|
|
}
|
|
|
|
.delta.up {
|
|
color: var(--success-deep);
|
|
}
|
|
|
|
.delta.down {
|
|
color: var(--danger);
|
|
}
|
|
|
|
.delta.flat {
|
|
color: var(--muted);
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.kpi {
|
|
padding: var(--sp-3);
|
|
}
|
|
|
|
.kpi-value {
|
|
font-size: var(--fs-xl);
|
|
}
|
|
}
|
|
</style>
|