Switch admin auth to mobile, restyle shell/login with Vuesax icons, and add product detail fields plus public product pages. Co-authored-by: Cursor <cursoragent@cursor.com>
88 lines
3.8 KiB
Vue
88 lines
3.8 KiB
Vue
<script setup lang="ts">
|
||
import { type DashboardDto, MESSAGE_STATUS_LABELS } from "@imen/shared";
|
||
import { message } from "ant-design-vue";
|
||
import { onMounted, ref } from "vue";
|
||
import { api, errorMessage } from "@/api/http";
|
||
import VuesaxIcon from "@/components/VuesaxIcon.vue";
|
||
import PageHeader from "@/components/PageHeader.vue";
|
||
import { activityLabel, faNumber, jalali } from "@/utils/format";
|
||
|
||
const data = ref<DashboardDto | null>(null);
|
||
|
||
onMounted(async () => {
|
||
try {
|
||
data.value = await api<DashboardDto>("/admin/dashboard");
|
||
} catch (e) {
|
||
void message.error(errorMessage(e));
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<PageHeader title="داشبورد" subtitle="خلاصه وضعیت محتوای سایت">
|
||
<RouterLink :to="{ name: 'product-new' }">
|
||
<a-button type="primary">
|
||
<template #icon><VuesaxIcon name="add" :size="16" /></template>
|
||
محصول جدید
|
||
</a-button>
|
||
</RouterLink>
|
||
</PageHeader>
|
||
|
||
<a-spin :spinning="!data">
|
||
<div class="grid grid-cols-2 gap-4 lg:grid-cols-5">
|
||
<RouterLink :to="{ name: 'products' }">
|
||
<a-card hoverable><a-statistic title="محصولات" :value="faNumber(data?.products ?? 0)" /></a-card>
|
||
</RouterLink>
|
||
<RouterLink :to="{ name: 'products', query: { status: 'draft' } }">
|
||
<a-card hoverable><a-statistic title="پیشنویس" :value="faNumber(data?.draftProducts ?? 0)" /></a-card>
|
||
</RouterLink>
|
||
<RouterLink :to="{ name: 'categories' }">
|
||
<a-card hoverable><a-statistic title="دستهبندیها" :value="faNumber(data?.categories ?? 0)" /></a-card>
|
||
</RouterLink>
|
||
<RouterLink :to="{ name: 'messages', query: { status: 'NEW' } }">
|
||
<a-card hoverable>
|
||
<a-statistic
|
||
title="پیامهای جدید"
|
||
:value="faNumber(data?.newMessages ?? 0)"
|
||
:value-style="data?.newMessages ? { color: '#ff5a00' } : undefined"
|
||
/>
|
||
</a-card>
|
||
</RouterLink>
|
||
<RouterLink :to="{ name: 'media' }">
|
||
<a-card hoverable><a-statistic title="تصاویر" :value="faNumber(data?.media ?? 0)" /></a-card>
|
||
</RouterLink>
|
||
</div>
|
||
|
||
<div class="mt-6 grid gap-6 lg:grid-cols-2">
|
||
<a-card title="آخرین پیامهای تماس">
|
||
<template #extra><RouterLink :to="{ name: 'messages' }">همه پیامها</RouterLink></template>
|
||
<a-empty v-if="!data?.recentMessages.length" description="پیامی ثبت نشده است" />
|
||
<a-list v-else :data-source="data.recentMessages" item-layout="horizontal">
|
||
<template #renderItem="{ item }">
|
||
<a-list-item>
|
||
<a-list-item-meta :title="`${item.name} — ${item.org}`" :description="item.message || item.phone" />
|
||
<div class="text-left text-xs text-gray-400">
|
||
<a-tag :color="item.status === 'NEW' ? 'orange' : 'default'">{{ MESSAGE_STATUS_LABELS[item.status as keyof typeof MESSAGE_STATUS_LABELS] }}</a-tag>
|
||
<div class="mt-1">{{ jalali(item.createdAt) }}</div>
|
||
</div>
|
||
</a-list-item>
|
||
</template>
|
||
</a-list>
|
||
</a-card>
|
||
|
||
<a-card title="آخرین فعالیتها">
|
||
<a-empty v-if="!data?.recentActivity.length" description="فعالیتی ثبت نشده است" />
|
||
<a-timeline v-else class="!mt-2">
|
||
<a-timeline-item v-for="a in data.recentActivity" :key="a.id">
|
||
<div class="text-sm">
|
||
<b>{{ activityLabel(a.action, a.entity) }}</b>
|
||
<span v-if="a.summary" class="text-gray-600"> — {{ a.summary }}</span>
|
||
</div>
|
||
<div class="text-xs text-gray-400">{{ a.user ?? "سیستم" }} · {{ jalali(a.createdAt) }}</div>
|
||
</a-timeline-item>
|
||
</a-timeline>
|
||
</a-card>
|
||
</div>
|
||
</a-spin>
|
||
</template>
|