feat: routines module; super admin subscription badge, 7-day trial and lifetime plan
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user