feat: migrate CRM screens to Ant Design Vue and expand home dashboard
Preserve the local Ant UI work across views/components and add the new home dashboard chart panels before merging latest main.
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import type { Component } from 'vue';
|
||||
import {
|
||||
AlertOutlined,
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
LeftOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { toFaDigits } from '@/lib/jalali';
|
||||
import type { HomeDashboardAlertDto } from '@mento/shared';
|
||||
|
||||
const props = defineProps<{
|
||||
alerts: HomeDashboardAlertDto[];
|
||||
}>();
|
||||
|
||||
type Tone = HomeDashboardAlertDto['tone'];
|
||||
|
||||
type AlertRow = {
|
||||
key: string;
|
||||
tone: Tone;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
link: string;
|
||||
};
|
||||
|
||||
const TONE_ORDER: Record<Tone, number> = {
|
||||
urgent: 0,
|
||||
warning: 1,
|
||||
info: 2,
|
||||
success: 3,
|
||||
};
|
||||
|
||||
const rows = computed<AlertRow[]>(() =>
|
||||
[...(props.alerts ?? [])]
|
||||
.map((a) => {
|
||||
const parts = a.text.split(/\s*[—\-–]\s*|\n/);
|
||||
const title = parts[0]?.trim() || a.text;
|
||||
const subtitle = parts.slice(1).join(' ').trim() || undefined;
|
||||
return {
|
||||
key: a.key,
|
||||
tone: a.tone,
|
||||
title,
|
||||
subtitle,
|
||||
link: a.link || '/',
|
||||
};
|
||||
})
|
||||
.sort((a, b) => TONE_ORDER[a.tone] - TONE_ORDER[b.tone]),
|
||||
);
|
||||
|
||||
const urgentCount = computed(() => rows.value.filter((r) => r.tone === 'urgent').length);
|
||||
const warningCount = computed(() => rows.value.filter((r) => r.tone === 'warning').length);
|
||||
|
||||
function toneIcon(tone: Tone): Component {
|
||||
if (tone === 'urgent') return ExclamationCircleOutlined;
|
||||
if (tone === 'warning') return AlertOutlined;
|
||||
if (tone === 'success') return CheckCircleOutlined;
|
||||
return InfoCircleOutlined;
|
||||
}
|
||||
|
||||
function toneLabel(tone: Tone) {
|
||||
if (tone === 'urgent') return 'فوری';
|
||||
if (tone === 'warning') return 'هشدار';
|
||||
if (tone === 'success') return 'موفق';
|
||||
return 'اطلاع';
|
||||
}
|
||||
|
||||
function actionLabel(tone: Tone) {
|
||||
if (tone === 'urgent') return 'اقدام';
|
||||
if (tone === 'warning') return 'بررسی';
|
||||
return 'مشاهده';
|
||||
}
|
||||
|
||||
function rowClass(tone: Tone) {
|
||||
if (tone === 'urgent') return 'border-rose-200/80 bg-rose-50/50 hover:bg-rose-50';
|
||||
if (tone === 'warning') return 'border-amber-200/80 bg-amber-50/50 hover:bg-amber-50';
|
||||
if (tone === 'success') return 'border-emerald-200/70 bg-emerald-50/40 hover:bg-emerald-50/80';
|
||||
return 'border-slate-200/80 bg-slate-50/70 hover:bg-slate-100/80';
|
||||
}
|
||||
|
||||
function barClass(tone: Tone) {
|
||||
if (tone === 'urgent') return 'bg-rose-500';
|
||||
if (tone === 'warning') return 'bg-amber-500';
|
||||
if (tone === 'success') return 'bg-emerald-500';
|
||||
return 'bg-blue-500';
|
||||
}
|
||||
|
||||
function iconWrapClass(tone: Tone) {
|
||||
if (tone === 'urgent') return 'bg-rose-100 text-rose-600';
|
||||
if (tone === 'warning') return 'bg-amber-100 text-amber-600';
|
||||
if (tone === 'success') return 'bg-emerald-100 text-emerald-600';
|
||||
return 'bg-blue-100 text-blue-600';
|
||||
}
|
||||
|
||||
function badgeClass(tone: Tone) {
|
||||
if (tone === 'urgent') return 'bg-rose-100 text-rose-700';
|
||||
if (tone === 'warning') return 'bg-amber-100 text-amber-700';
|
||||
if (tone === 'success') return 'bg-emerald-100 text-emerald-700';
|
||||
return 'bg-blue-100 text-blue-700';
|
||||
}
|
||||
|
||||
function actionClass(tone: Tone) {
|
||||
if (tone === 'urgent') return 'text-rose-600 group-hover:text-rose-700';
|
||||
if (tone === 'warning') return 'text-amber-600 group-hover:text-amber-700';
|
||||
if (tone === 'success') return 'text-emerald-600 group-hover:text-emerald-700';
|
||||
return 'text-blue-600 group-hover:text-blue-700';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
v-if="rows.length"
|
||||
class="bg-white rounded-2xl border border-slate-200/80 p-5 shadow-sm flex flex-col h-[330px]"
|
||||
>
|
||||
<header class="flex items-center justify-between gap-3 pb-3 mb-3 border-b border-slate-100 shrink-0">
|
||||
<div class="flex items-center gap-2.5 min-w-0">
|
||||
<span class="relative flex h-2.5 w-2.5 shrink-0" aria-hidden="true">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-75" />
|
||||
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-rose-500" />
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<h3 class="m-0 font-bold text-slate-800 text-base leading-6">نیازمند توجه</h3>
|
||||
<p class="m-0 mt-0.5 text-xs text-slate-400 truncate">
|
||||
موارد با اولویت بالاتر اول آمدهاند
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 shrink-0">
|
||||
<span
|
||||
v-if="urgentCount"
|
||||
class="text-[11px] font-bold bg-rose-50 text-rose-600 border border-rose-200/70 px-2 py-0.5 rounded-full"
|
||||
>
|
||||
{{ toFaDigits(urgentCount) }} فوری
|
||||
</span>
|
||||
<span
|
||||
v-if="warningCount"
|
||||
class="text-[11px] font-bold bg-amber-50 text-amber-700 border border-amber-200/70 px-2 py-0.5 rounded-full"
|
||||
>
|
||||
{{ toFaDigits(warningCount) }} هشدار
|
||||
</span>
|
||||
<span class="text-[11px] font-semibold text-slate-500 bg-slate-100 px-2 py-0.5 rounded-full">
|
||||
{{ toFaDigits(rows.length) }} مورد
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 overflow-y-auto space-y-2 pe-1 min-h-0">
|
||||
<RouterLink
|
||||
v-for="item in rows"
|
||||
:key="item.key"
|
||||
:to="item.link"
|
||||
class="group relative overflow-hidden rounded-xl border px-3 py-2.5 flex items-center gap-3 transition-all"
|
||||
:class="rowClass(item.tone)"
|
||||
>
|
||||
<span
|
||||
class="absolute inset-y-0 right-0 w-1 rounded-s-full"
|
||||
:class="barClass(item.tone)"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="w-8 h-8 rounded-lg flex items-center justify-center shrink-0 text-sm"
|
||||
:class="iconWrapClass(item.tone)"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<component :is="toneIcon(item.tone)" />
|
||||
</span>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<h4 class="m-0 font-bold text-[13px] text-slate-800 leading-5 truncate">
|
||||
{{ item.title }}
|
||||
</h4>
|
||||
<div class="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<span
|
||||
class="text-[10px] font-bold px-1.5 py-0.5 rounded shrink-0"
|
||||
:class="badgeClass(item.tone)"
|
||||
>
|
||||
{{ toneLabel(item.tone) }}
|
||||
</span>
|
||||
<span v-if="item.subtitle" class="text-[11px] text-slate-500 truncate">
|
||||
{{ item.subtitle }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="shrink-0 flex items-center gap-1 text-[11px] font-semibold"
|
||||
:class="actionClass(item.tone)"
|
||||
>
|
||||
<span class="opacity-0 group-hover:opacity-100 transition-opacity hidden sm:inline">
|
||||
{{ actionLabel(item.tone) }}
|
||||
</span>
|
||||
<LeftOutlined class="text-slate-400 group-hover:-translate-x-0.5 transition-transform" />
|
||||
</div>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
Reference in New Issue
Block a user